Skip to content

Commit

Permalink
feat: wip
Browse files Browse the repository at this point in the history
  • Loading branch information
nagyben committed Apr 22, 2024
1 parent e5b635b commit 11dfed9
Show file tree
Hide file tree
Showing 12 changed files with 326 additions and 131 deletions.
4 changes: 4 additions & 0 deletions .config/config.json5
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
"<Ctrl-z>": "Suspend", // Suspend the application
"j": "Down", // Move down
"k": "Up", // Move up
"down": "Down", // Move down
"up": "Up", // Move up
"r": "GetRepos", // Refresh the list of repositories
"enter": "Enter"
},
}
}
34 changes: 34 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ build = "build.rs"

[dependencies]
better-panic = "0.3.0"
chrono = "0.4.38"
clap = { version = "4.4.5", features = [
"derive",
"cargo",
Expand All @@ -32,6 +33,7 @@ lazy_static = "1.4.0"
libc = "0.2.148"
log = "0.4.20"
octocrab = "0.38.0"
open = "5.1.2"
pretty_assertions = "1.4.0"
ratatui = { version = "0.26.0", features = ["serde", "macros"] }
serde = { version = "1.0.188", features = ["derive"] }
Expand Down
5 changes: 5 additions & 0 deletions src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use serde::{
};
use strum::Display;

use crate::components::pull_request::PullRequest;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Display, Deserialize)]
pub enum Action {
Tick,
Expand All @@ -21,4 +23,7 @@ pub enum Action {
// custom actions
Up,
Down,
GetReposResult(Vec<PullRequest>),
GetRepos,
Enter,
}
6 changes: 3 additions & 3 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use tokio::sync::mpsc;

use crate::{
action::Action,
components::{fps::FpsCounter, home::Home, keystrokes::Keystrokes, repo_list::RepoList, Component},
components::{fps::FpsCounter, home::Home, keystrokes::Keystrokes, repo_list::PullRequestList, Component},
config::Config,
mode::Mode,
tui,
Expand All @@ -26,14 +26,14 @@ pub struct App {
impl App {
pub fn new(tick_rate: f64, frame_rate: f64) -> Result<Self> {
let home = Home::new();
let pr_list = PullRequestList::default();
let keystrokes = Keystrokes::default();
let file_list = RepoList::default();
let config = Config::new()?;
let mode = Mode::Normal;
Ok(Self {
tick_rate,
frame_rate,
components: vec![Box::new(home), Box::new(keystrokes), Box::new(file_list)],
components: vec![Box::new(home), Box::new(keystrokes), Box::new(pr_list)],
should_quit: false,
should_suspend: false,
config,
Expand Down
1 change: 1 addition & 0 deletions src/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::{
pub mod fps;
pub mod home;
pub mod keystrokes;
pub mod pull_request;
pub mod repo_list;

/// `Component` is a trait that represents a visual and interactive element of the user interface.
Expand Down
44 changes: 44 additions & 0 deletions src/components/pull_request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
type URI = String;
type DateTime = chrono::DateTime<chrono::Utc>;

use graphql_client::GraphQLQuery;
use serde::{Deserialize, Serialize};

use self::pull_requests_query::{PullRequestsQuerySearchEdgesNode, PullRequestsQuerySearchEdgesNodeOnPullRequest};
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "src/github/schema.graphql",
query_path = "src/github/queries/pull_requests.graphql",
variables_derives = "Clone, Debug",
response_derives = "Clone, Debug"
)]
pub struct PullRequestsQuery;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PullRequest {
pub number: usize,
pub title: String,
pub repository: String,
pub created_at: DateTime,
pub updated_at: DateTime,
pub url: URI,
pub changed_files: usize,
pub additions: usize,
pub deletions: usize,
}

impl From<&PullRequestsQuerySearchEdgesNodeOnPullRequest> for PullRequest {
fn from(value: &PullRequestsQuerySearchEdgesNodeOnPullRequest) -> Self {
Self {
number: value.number as usize,
title: value.title.clone(),
repository: value.repository.name_with_owner.clone(),
created_at: value.created_at,
updated_at: value.updated_at,
url: value.url.clone(),
changed_files: value.changed_files as usize,
additions: value.additions as usize,
deletions: value.deletions as usize,
}
}
}
102 changes: 84 additions & 18 deletions src/components/repo_list.rs
Original file line number Diff line number Diff line change
@@ -1,39 +1,75 @@
use std::{collections::HashMap, time::Duration};

use color_eyre::eyre::Result;
use color_eyre::{eyre::Result, owo_colors::OwoColorize};
use crossterm::event::{KeyCode, KeyEvent};
use graphql_client::GraphQLQuery;
use octocrab::Octocrab;
use ratatui::{prelude::*, widgets::*};
use ratatui::{
prelude::*,
widgets::{
block::{Position, Title},
*,
},
};
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc::UnboundedSender;

use super::{Component, Frame};
use super::pull_request;
use crate::{
action::Action,
components::{
pull_request::{pull_requests_query, PullRequest, PullRequestsQuery},
Component, Frame,
},
config::{Config, KeyBindings},
};

#[derive(Default)]
pub struct RepoList {
pub struct PullRequestList {
command_tx: Option<UnboundedSender<Action>>,
config: Config,
selected_row: usize,
pull_requests: Vec<PullRequest>,
}

impl RepoList {
impl PullRequestList {
pub fn new() -> Self {
Self::default()
}

fn fetch_repos(&mut self) -> Result<()> {
let token = std::env::var("GITHUB_TOKEN").expect("GITHUB_TOKEN must be set");
let oc = Octocrab::builder().personal_token(token).build().expect("Failed to create Octocrab client");
let repos = oc.graphql({}).await?;
let tx = self.command_tx.clone().unwrap();
tokio::spawn(async move {
let token = std::env::var("GITHUB_TOKEN").expect("GITHUB_TOKEN must be set");
let oc = Octocrab::builder().personal_token(token).build().expect("Failed to create Octocrab client");
let response: octocrab::Result<graphql_client::Response<pull_requests_query::ResponseData>> =
oc.graphql(&PullRequestsQuery::build_query(pull_requests_query::Variables {})).await;
match response {
Ok(response) => {
let r = response.data.unwrap().search.edges.unwrap();
let pull_requests: Vec<PullRequest> = r
.iter()
.map(|v: &Option<pull_requests_query::PullRequestsQuerySearchEdges>| {
let inner = v.as_ref().unwrap().node.as_ref().unwrap();
match inner {
pull_requests_query::PullRequestsQuerySearchEdgesNode::PullRequest(pr) => pr.into(),
_ => panic!("Unexpected node type: {:?}", inner),
}
})
.collect();
pull_requests.iter().for_each(|pr| {});
tx.send(Action::GetReposResult(pull_requests)).unwrap();
},
Err(e) => {
tx.send(Action::Error(e.to_string())).unwrap();
},
}
});
Ok(())
}
}

impl Component for RepoList {
impl Component for PullRequestList {
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
self.command_tx = Some(tx);
Ok(())
Expand All @@ -52,46 +88,76 @@ impl Component for RepoList {
return Ok(Some(Action::Render));
},
Action::Down => {
self.selected_row = self.selected_row.saturating_add(1);
self.selected_row = std::cmp::min(self.selected_row + 1, self.pull_requests.len() - 1);
return Ok(Some(Action::Render));
},
Action::GetRepos => {
self.fetch_repos()?;
},
Action::GetReposResult(pull_requests) => {
self.pull_requests = pull_requests;
},
Action::Enter => {
let pr = self.pull_requests.get(self.selected_row).unwrap();
let url = pr.url.clone();
let _ = open::that(url);
},
_ => {},
}
Ok(None)
}

fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> {
let rows = vec![Row::new(vec!["Cell11", "Cell12"]); 10];
let rows = self
.pull_requests
.iter()
.map(|pr: &PullRequest| {
Row::new(vec![
Cell::from(format!("{:}", pr.number)),
Cell::from(pr.repository.clone()),
Cell::from(pr.title.clone()),
Cell::from(format!("{}", pr.created_at.format("%Y-%m-%d %H:%M"))),
Cell::from(format!("{}", pr.updated_at.format("%Y-%m-%d %H:%M"))),
Cell::from(Line::from(vec![
Span::styled(format!("{:+}", pr.additions), Style::new().fg(Color::LightGreen)),
Span::styled(format!("{:+}", (0 - pr.deletions as isize)), Style::new().fg(Color::LightRed)),
])),
])
})
.collect::<Vec<_>>();
let mut table_state = TableState::default();
table_state.select(Some(self.selected_row));
let table = Table::default()
.widths(Constraint::from_lengths([5, 5]))
.widths(Constraint::from_lengths([4, 30, 60, 20, 20, 6, 6]))
.rows(rows)
.column_spacing(1)
.header(Row::new(vec!["Col1", "Col2"]).bottom_margin(1))
.footer(Row::new(vec!["Footer1", "Footer2"]))
.block(Block::default().title("Title"))
.header(Row::new(vec!["#", "Title", "Repository", "Created", "Updated"]).bottom_margin(1))
.block(
Block::default()
.title(Title::from("Pull Requests"))
.borders(Borders::ALL)
.border_type(BorderType::Rounded),
)
.highlight_style(Style::new().reversed().add_modifier(Modifier::BOLD))
.highlight_symbol(">> ");

f.render_stateful_widget(table, area, &mut table_state);
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_new() {
let item_list = RepoList::new();
let item_list = PullRequestList::new();
assert_eq!(item_list.selected_row, 0);
}

#[test]
fn test_up_down_actions() {
let mut item_list = RepoList::new();
let mut item_list = PullRequestList::new();
assert_eq!(item_list.update(Action::Up).unwrap(), Some(Action::Render));
assert_eq!(item_list.update(Action::Down).unwrap(), Some(Action::Render));
}
Expand Down
Loading

0 comments on commit 11dfed9

Please sign in to comment.