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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/target
.data/*.log
/.env
tmp/*
!.gitkeep
35 changes: 34 additions & 1 deletion Cargo.lock

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

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,6 @@ codegen-units = 1
lto = true
opt-level = "s"
strip = true

[dev-dependencies]
insta = { version = "1.48.0", features = ["redactions"] }
4 changes: 3 additions & 1 deletion src/action.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::path::PathBuf;

use serde::{Deserialize, Serialize};
use strum::Display;

Expand All @@ -12,7 +14,7 @@ pub enum Action {
ClearScreen,
Error(String),
Help,
WorktreeUpdate,
WorktreeUpdate(Option<PathBuf>),
WorktreeOpenEditor(String),
WorktreeNext,
WorktreePrevious,
Expand Down
33 changes: 21 additions & 12 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crossterm::event::KeyEvent;
use ratatui::prelude::Rect;
use ratatui::{Frame, prelude::Rect};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tokio::sync::mpsc;
use tracing::{debug, info};

Expand All @@ -12,6 +13,7 @@ use crate::{
};

pub struct App {
path: Option<PathBuf>,
config: Config,
tick_rate: f64,
frame_rate: f64,
Expand All @@ -31,12 +33,13 @@ pub enum Mode {
}

impl App {
pub fn new(tick_rate: f64, frame_rate: f64) -> color_eyre::Result<Self> {
pub fn new(tick_rate: f64, frame_rate: f64, path: Option<PathBuf>) -> color_eyre::Result<Self> {
let (action_tx, action_rx) = mpsc::unbounded_channel();
Ok(Self {
path: path.clone(),
tick_rate,
frame_rate,
components: vec![Box::new(WorktreeList::new())],
components: vec![Box::new(WorktreeList::new(path)?)],
should_quit: false,
should_suspend: false,
config: Config::new()?,
Expand Down Expand Up @@ -66,7 +69,7 @@ impl App {

let action_tx = self.action_tx.clone();

action_tx.send(Action::WorktreeUpdate)?;
action_tx.send(Action::WorktreeUpdate(self.path.clone()))?;
loop {
self.handle_events(&mut tui).await?;
self.handle_actions(&mut tui)?;
Expand Down Expand Up @@ -163,15 +166,21 @@ impl App {
Ok(())
}

fn render(&mut self, tui: &mut Tui) -> color_eyre::Result<()> {
tui.draw(|frame| {
for component in self.components.iter_mut() {
if let Err(err) = component.draw(frame, frame.area()) {
let _ = self
.action_tx
.send(Action::Error(format!("Failed to draw: {:?}", err)));
}
pub fn draw(&mut self, frame: &mut Frame, _area: Rect) -> color_eyre::Result<()> {
for component in self.components.iter_mut() {
if let Err(err) = component.draw(frame, frame.area()) {
let _ = self
.action_tx
.send(Action::Error(format!("Failed to draw: {:?}", err)));
}
}
Ok(())
}

pub fn render(&mut self, tui: &mut Tui) -> color_eyre::Result<()> {
tui.draw(|frame| {
self.draw(frame, frame.area())
.expect("failed to draw to terminal")
})?;
Ok(())
}
Expand Down
55 changes: 40 additions & 15 deletions src/components/worktree_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,60 @@ use ratatui::layout::{Constraint, Rect};
use ratatui::style::{Color, Style};
use ratatui::widgets::{Block, Padding, Row, Table, TableState};
use std::io::stdout;
use std::path::PathBuf;
use std::process::Command;
use tokio::sync::mpsc::UnboundedSender;

#[derive(Default)]
pub struct WorktreeList {
repo: GitRepo,
command_tx: Option<UnboundedSender<Action>>,
config: Config,
table_items: Vec<GitWorktree>,
table_state: TableState,
}

impl WorktreeList {
pub fn new() -> Self {
Self::default()
pub fn new(path: Option<PathBuf>) -> color_eyre::Result<Self> {
let repo = GitRepo::new(path);
let mut table_state = TableState::default();
let mut table_items = vec![];

let worktrees = repo.get_worktrees().context("Unable to get worktrees")?;
table_state.select(Some(0));
for worktree in worktrees {
table_items.push(worktree);
}
Ok(Self {
repo,
table_items,
table_state,
..Default::default()
})
}

fn run_editor(&self, path: &str) -> color_eyre::Result<()> {
stdout().execute(LeaveAlternateScreen)?;
disable_raw_mode()?;
//TODO: should call configured editor or fallback to $EDITOR
Command::new("zed").arg(path).status()?;
Ok(())
}

fn update_repos(&mut self, _path: Option<PathBuf>) -> color_eyre::Result<()> {
let worktrees = self
.repo
.get_worktrees()
.context("Unable to get worktrees")?;
if !&worktrees.is_empty() && self.table_state.selected().is_none() {
self.table_state.select(Some(0))
}
for worktree in worktrees {
self.table_items.push(worktree);
}

Ok(())
}
}

impl Component for WorktreeList {
Expand All @@ -50,8 +82,8 @@ impl Component for WorktreeList {
match key.code {
KeyCode::Enter if self.table_state.selected().is_some() => {
if let Some(current_path_index) = self.table_state.selected() {
let path = &self.table_items[current_path_index];
return Ok(Some(Action::WorktreeOpenEditor(path.path.to_string())));
let workspace = &self.table_items[current_path_index];
return Ok(Some(Action::WorktreeOpenEditor(workspace.path.to_string())));
}
color_eyre::eyre::bail!("expected to have selected path");
}
Expand Down Expand Up @@ -79,15 +111,8 @@ impl Component for WorktreeList {
Action::Render => {
// add any logic here that should run on every render
}
Action::WorktreeUpdate => {
let repo = GitRepo::new();
let worktrees = repo.get_worktrees().context("Unable to get worktrees")?;
if !&worktrees.is_empty() && self.table_state.selected().is_none() {
self.table_state.select(Some(0))
}
for worktree in worktrees {
self.table_items.push(worktree);
}
Action::WorktreeUpdate(path) => {
self.update_repos(path)?;
}
Action::WorktreePrevious => {
self.table_state.select_previous();
Expand Down Expand Up @@ -115,7 +140,7 @@ impl Component for WorktreeList {
let rows: Vec<Row> = self
.table_items
.iter()
.map(|item| Row::new([item.branch.clone(), item.path.to_string()]))
.map(|item| Row::new([item.branch.clone(), item.path.get_short_path()]))
.collect();

let widths = [Constraint::Percentage(20), Constraint::Percentage(80)];
Expand All @@ -135,7 +160,7 @@ impl Component for WorktreeList {
.style(Color::White)
.row_highlight_style(Style::new().on_black().bold())
.column_highlight_style(Color::Gray)
.highlight_symbol("🐸 ");
.highlight_symbol("> ");

frame.render_stateful_widget(table, area, &mut self.table_state);
Ok(())
Expand Down
14 changes: 14 additions & 0 deletions src/git_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ impl GitPath {
pub fn new(path_buf: PathBuf) -> Self {
Self(path_buf)
}

pub fn get_short_path(&self) -> String {
let mut short_path = String::new();
let mut parts = self.0.iter().rev();
// let mut parts = self.0.ancestors();
for _ in 0..3 {
if let Some(part) = parts.next()
&& let Some(part) = part.to_str()
{
short_path = format!("{}/{}", part, short_path,);
}
}
short_path
}
}

impl std::fmt::Display for GitPath {
Expand Down
18 changes: 13 additions & 5 deletions src/git_repo.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
use std::path::PathBuf;

use git2::Repository;

use crate::{git_path::GitPath, git_worktree::GitWorktree};

#[derive(Debug)]
pub struct GitRepo {}
#[derive(Debug, Default)]
pub struct GitRepo {
path: Option<PathBuf>,
}

impl GitRepo {
pub fn new() -> Self {
Self {}
pub fn new(path: Option<PathBuf>) -> Self {
Self { path }
}
fn get_repo(&self) -> Result<Repository, git2::Error> {
Repository::open_from_env()
if let Some(path) = &self.path {
Repository::open(path)
} else {
Repository::open_from_env()
}
}

pub fn get_worktrees(&self) -> Result<Vec<GitWorktree>, git2::Error> {
Expand Down
8 changes: 8 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
pub mod action;
pub mod app;
pub mod components;
pub mod config;
pub mod git_path;
pub mod git_repo;
pub mod git_worktree;
pub mod tui;
3 changes: 2 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ async fn main() -> color_eyre::Result<()> {
crate::logging::init()?;

let args = Cli::parse();
let mut app = App::new(args.tick_rate, args.frame_rate)?;
//TODO: add path to args list
let mut app = App::new(args.tick_rate, args.frame_rate, None)?;
app.run().await?;
Ok(())
}
42 changes: 42 additions & 0 deletions tests/common/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use std::path::PathBuf;

use git2::{Repository, RepositoryInitOptions, WorktreeAddOptions};

pub struct Fixture {
pub path: PathBuf,
pub repo: Repository,
}

impl Fixture {
pub fn new(path: &PathBuf) -> Self {
let mut opts = RepositoryInitOptions::new();
opts.initial_head("main");
let repo = Repository::init_opts(path, &opts).unwrap();

{
let mut config = repo.config().unwrap();
config.set_str("user.name", "name").unwrap();
config.set_str("user.email", "email").unwrap();
let mut index = repo.index().unwrap();
let id = index.write_tree().unwrap();

let tree = repo.find_tree(id).unwrap();
let sig = repo.signature().unwrap();
repo.commit(Some("HEAD"), &sig, &sig, "initial\n\nbody", &tree, &[])
.unwrap();
}
Self {
repo,
path: path.to_path_buf(),
}
}

pub fn create_worktree(&mut self, name: &str) {
let opts = WorktreeAddOptions::new();

let _ = self
.repo
.worktree(name, &self.path.join(name), Some(&opts))
.unwrap();
}
}
2 changes: 2 additions & 0 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod helpers;
pub mod temp_directory;
Loading
Loading