use anyhow::Result;
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{Terminal, backend::CrosstermBackend};
use std::io;
use super::event::{Event, EventHandler};
use super::ui;
use super::views::{View, ViewState};
use crate::api::BitbucketClient;
use crate::models::{Issue, Pipeline, PullRequest, Repository};
pub struct App {
pub running: bool,
pub current_view: View,
pub view_state: ViewState,
pub client: Option<BitbucketClient>,
pub workspace: Option<String>,
pub status: Option<String>,
pub loading: bool,
pub error: Option<String>,
pub repositories: Vec<Repository>,
pub pull_requests: Vec<PullRequest>,
pub issues: Vec<Issue>,
pub pipelines: Vec<Pipeline>,
}
impl App {
pub fn new() -> Self {
Self {
running: true,
current_view: View::Dashboard,
view_state: ViewState::default(),
client: None,
workspace: None,
status: None,
loading: false,
error: None,
repositories: Vec::new(),
pull_requests: Vec::new(),
issues: Vec::new(),
pipelines: Vec::new(),
}
}
pub fn with_client(mut self, client: BitbucketClient) -> Self {
self.client = Some(client);
self
}
pub fn with_workspace(mut self, workspace: String) -> Self {
self.workspace = Some(workspace);
self
}
pub fn set_status(&mut self, message: &str) {
self.status = Some(message.to_string());
}
pub fn clear_status(&mut self) {
self.status = None;
}
pub fn set_error(&mut self, message: &str) {
self.error = Some(message.to_string());
}
pub fn clear_error(&mut self) {
self.error = None;
}
pub fn switch_view(&mut self, view: View) {
self.current_view = view;
self.view_state.selected_index = 0;
self.clear_error();
}
pub fn handle_key(&mut self, key: crossterm::event::KeyEvent) {
use crossterm::event::KeyCode;
match key.code {
KeyCode::Char('q') => {
self.running = false;
return;
}
KeyCode::Char('1') => {
self.switch_view(View::Dashboard);
return;
}
KeyCode::Char('2') => {
self.switch_view(View::Repositories);
return;
}
KeyCode::Char('3') => {
self.switch_view(View::PullRequests);
return;
}
KeyCode::Char('4') => {
self.switch_view(View::Issues);
return;
}
KeyCode::Char('5') => {
self.switch_view(View::Pipelines);
return;
}
KeyCode::Esc => {
self.clear_error();
return;
}
_ => {}
}
match key.code {
KeyCode::Up | KeyCode::Char('k') => {
self.view_state.previous();
}
KeyCode::Down | KeyCode::Char('j') => {
let max = match self.current_view {
View::Dashboard => 4,
View::Repositories => self.repositories.len(),
View::PullRequests => self.pull_requests.len(),
View::Issues => self.issues.len(),
View::Pipelines => self.pipelines.len(),
};
self.view_state.next(max);
}
KeyCode::Enter => {
self.handle_select();
}
KeyCode::Char('r') => {
}
_ => {}
}
}
fn handle_select(&mut self) {
match self.current_view {
View::Dashboard => {
match self.view_state.selected_index {
0 => self.switch_view(View::Repositories),
1 => self.switch_view(View::PullRequests),
2 => self.switch_view(View::Issues),
3 => self.switch_view(View::Pipelines),
_ => {}
}
}
View::Repositories => {
if let Some(repo) = self.repositories.get(self.view_state.selected_index) {
self.set_status(&format!("Selected: {}", repo.full_name));
}
}
View::PullRequests => {
if let Some(pr) = self.pull_requests.get(self.view_state.selected_index) {
self.set_status(&format!("Selected PR #{}: {}", pr.id, pr.title));
}
}
View::Issues => {
if let Some(issue) = self.issues.get(self.view_state.selected_index) {
self.set_status(&format!("Selected Issue #{}: {}", issue.id, issue.title));
}
}
View::Pipelines => {
if let Some(pipeline) = self.pipelines.get(self.view_state.selected_index) {
self.set_status(&format!("Selected Pipeline #{}", pipeline.build_number));
}
}
}
}
pub fn quit(&mut self) {
self.running = false;
}
pub async fn load_repositories(&mut self) -> Result<()> {
if let (Some(client), Some(workspace)) = (&self.client, &self.workspace) {
self.loading = true;
match client.list_repositories(workspace, None, Some(50)).await {
Ok(result) => {
self.repositories = result.values;
self.clear_error();
}
Err(e) => {
self.set_error(&format!("Failed to load repositories: {}", e));
}
}
self.loading = false;
} else {
self.set_error("No workspace configured");
}
Ok(())
}
pub async fn load_pull_requests(&mut self) -> Result<()> {
if let (Some(client), Some(workspace)) = (&self.client, &self.workspace) {
self.loading = true;
self.pull_requests.clear();
if let Ok(repos) = client.list_repositories(workspace, None, Some(50)).await {
for repo in repos.values {
let repo_slug = repo.slug.as_deref().unwrap_or(&repo.name);
if let Ok(prs) = client
.list_pull_requests(workspace, repo_slug, None, None, Some(10))
.await
{
self.pull_requests.extend(prs.values);
}
}
}
self.clear_error();
self.loading = false;
} else {
self.set_error("No workspace configured");
}
Ok(())
}
pub async fn load_issues(&mut self) -> Result<()> {
if let (Some(client), Some(workspace)) = (&self.client, &self.workspace) {
self.loading = true;
self.issues.clear();
if let Ok(repos) = client.list_repositories(workspace, None, Some(50)).await {
for repo in repos.values {
let repo_slug = repo.slug.as_deref().unwrap_or(&repo.name);
if let Ok(issues) = client
.list_issues(workspace, repo_slug, None, None, Some(10))
.await
{
self.issues.extend(issues.values);
}
}
}
self.clear_error();
self.loading = false;
} else {
self.set_error("No workspace configured");
}
Ok(())
}
pub async fn load_pipelines(&mut self) -> Result<()> {
if let (Some(client), Some(workspace)) = (&self.client, &self.workspace) {
self.loading = true;
self.pipelines.clear();
if let Ok(repos) = client.list_repositories(workspace, None, Some(50)).await {
for repo in repos.values {
let repo_slug = repo.slug.as_deref().unwrap_or(&repo.name);
if let Ok(pipelines) = client
.list_pipelines(workspace, repo_slug, None, Some(10))
.await
{
self.pipelines.extend(pipelines.values);
}
}
}
self.clear_error();
self.loading = false;
} else {
self.set_error("No workspace configured");
}
Ok(())
}
pub async fn load_all_data(&mut self) -> Result<()> {
self.load_repositories().await?;
self.load_pull_requests().await?;
self.load_issues().await?;
self.load_pipelines().await?;
Ok(())
}
}
impl Default for App {
fn default() -> Self {
Self::new()
}
}
pub async fn run_tui(workspace: Option<String>) -> Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut app = App::new();
match BitbucketClient::from_stored().await {
Ok(client) => {
app = app.with_client(client);
if let Some(ws) = workspace {
app = app.with_workspace(ws);
} else {
app.set_error("No workspace specified. Use: bitbucket tui --workspace <workspace>");
}
}
Err(e) => {
app.set_error(&format!("Not authenticated: {}", e));
}
}
if app.workspace.is_some() && app.client.is_some() {
app.set_status("Loading data...");
terminal.draw(|f| ui::draw(f, &app))?;
if let Err(e) = app.load_repositories().await {
app.set_error(&format!("Failed to load data: {}", e));
} else {
app.set_status("Data loaded. Press 'r' to refresh.");
}
}
let event_handler = EventHandler::new(250);
let mut should_refresh = false;
while app.running {
terminal.draw(|f| ui::draw(f, &app))?;
if should_refresh && app.workspace.is_some() && app.client.is_some() {
should_refresh = false;
app.set_status("Refreshing...");
terminal.draw(|f| ui::draw(f, &app))?;
match app.current_view {
View::Dashboard | View::Repositories => {
let _ = app.load_repositories().await;
}
View::PullRequests => {
let _ = app.load_pull_requests().await;
}
View::Issues => {
let _ = app.load_issues().await;
}
View::Pipelines => {
let _ = app.load_pipelines().await;
}
}
app.set_status("Refreshed");
}
match event_handler.next()? {
Event::Key(key) => {
if let crossterm::event::KeyCode::Char('r') = key.code {
should_refresh = true;
}
app.handle_key(key);
}
Event::Tick => {
}
Event::Resize(_, _) => {
}
_ => {}
}
}
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
Ok(())
}