Compare commits

..

1 Commits

Author SHA1 Message Date
cuddle-please
9796040843 chore(release): 0.2.1
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-09-22 14:35:58 +00:00
10 changed files with 5 additions and 430 deletions

View File

@ -9,7 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.2.1] - 2024-09-22 ## [0.2.1] - 2024-09-22
### Added ### Added
- add spinner around download
- spawn a subshell for session - spawn a subshell for session
- implement git clone - implement git clone
- include vhs demo - include vhs demo
@ -21,7 +20,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- *(deps)* update rust crate bytes to v1.7.2 - *(deps)* update rust crate bytes to v1.7.2
### Other ### Other
- update gif to include spinner
- clean up ui - clean up ui
- build in cuddle instead of vhs - build in cuddle instead of vhs
- build first then run - build first then run

3
Cargo.lock generated
View File

@ -315,7 +315,6 @@ checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"crossterm_winapi", "crossterm_winapi",
"futures-core",
"mio", "mio",
"parking_lot", "parking_lot",
"rustix", "rustix",
@ -571,10 +570,8 @@ dependencies = [
"async-trait", "async-trait",
"bytes", "bytes",
"clap", "clap",
"crossterm",
"dirs", "dirs",
"dotenv", "dotenv",
"futures",
"gitea-rs", "gitea-rs",
"nucleo-matcher", "nucleo-matcher",
"octocrab", "octocrab",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 452 KiB

After

Width:  |  Height:  |  Size: 476 KiB

View File

@ -26,8 +26,6 @@ prost-types = "0.13.2"
bytes = "1.7.1" bytes = "1.7.1"
nucleo-matcher = "0.3.1" nucleo-matcher = "0.3.1"
ratatui = "0.28.1" ratatui = "0.28.1"
crossterm = { version = "0.28.0", features = ["event-stream"] }
futures = "0.3.30"
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"

View File

@ -3,7 +3,6 @@ use std::collections::BTreeMap;
use crate::{ use crate::{
app::App, app::App,
cache::CacheApp, cache::CacheApp,
components::inline_command::InlineCommand,
fuzzy_matcher::{FuzzyMatcher, FuzzyMatcherApp}, fuzzy_matcher::{FuzzyMatcher, FuzzyMatcherApp},
git_clone::GitCloneApp, git_clone::GitCloneApp,
git_provider::Repository, git_provider::Repository,
@ -76,17 +75,9 @@ impl RootCommand {
}; };
if clone { if clone {
let git_clone = self.app.git_clone(); self.app
.git_clone()
let mut wrap_cmd = .clone_repo(&repo, force_refresh)
InlineCommand::new(format!("cloning: {}", repo.to_rel_path().display()));
let repo = repo.clone();
wrap_cmd
.execute(move || async move {
git_clone.clone_repo(&repo, force_refresh).await?;
Ok(())
})
.await?; .await?;
} else { } else {
tracing::info!("skipping clone for repo: {}", &repo.to_rel_path().display()); tracing::info!("skipping clone for repo: {}", &repo.to_rel_path().display());

View File

@ -1,120 +0,0 @@
use anyhow::Error;
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
pub mod inline_command;
pub mod spinner;
#[derive(Debug, PartialEq)]
pub enum Msg {
Quit,
Tick,
Success,
Failure(String),
}
pub struct Command {
func: Box<CommandFunc>,
}
impl Command {
pub fn new<T: FnOnce(&Dispatch) -> Option<Msg> + 'static>(f: T) -> Self {
Self { func: Box::new(f) }
}
pub fn execute(self, dispatch: &Dispatch) -> Option<Msg> {
(self.func)(dispatch)
}
}
pub trait IntoCommand {
fn into_command(self) -> Command;
}
impl IntoCommand for () {
fn into_command(self) -> Command {
Command::new(|_| None)
}
}
impl IntoCommand for Command {
fn into_command(self) -> Command {
self
}
}
impl IntoCommand for Msg {
fn into_command(self) -> Command {
Command::new(|_| Some(self))
}
}
type CommandFunc = dyn FnOnce(&Dispatch) -> Option<Msg>;
pub fn create_dispatch() -> (Dispatch, Receiver) {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
(Dispatch { sender: tx }, Receiver { receiver: rx })
}
#[derive(Clone)]
pub struct Dispatch {
sender: UnboundedSender<Msg>,
}
impl Dispatch {
pub fn send(&self, msg: Msg) {
if let Err(e) = self.sender.send(msg) {
tracing::warn!("failed to send event: {}", e);
}
}
}
pub struct Receiver {
receiver: UnboundedReceiver<Msg>,
}
impl Receiver {
pub async fn next(&mut self) -> Option<Msg> {
self.receiver.recv().await
}
}
#[derive(Default)]
pub struct BatchCommand {
commands: Vec<Command>,
}
impl BatchCommand {
pub fn with(&mut self, cmd: impl IntoCommand) -> &mut Self {
self.commands.push(cmd.into_command());
self
}
}
impl IntoCommand for Vec<Command> {
fn into_command(self) -> Command {
BatchCommand::from(self).into_command()
}
}
impl From<Vec<Command>> for BatchCommand {
fn from(value: Vec<Command>) -> Self {
BatchCommand { commands: value }
}
}
impl IntoCommand for BatchCommand {
fn into_command(self) -> Command {
Command::new(|dispatch| {
for command in self.commands {
let msg = command.execute(dispatch);
if let Some(msg) = msg {
dispatch.send(msg);
}
}
None
})
}
}

View File

@ -1,194 +0,0 @@
use std::{io::Write, time::Duration};
use anyhow::Context;
use crossterm::event::{EventStream, KeyCode, KeyEventKind};
use futures::{FutureExt, StreamExt};
use ratatui::{
crossterm,
prelude::*,
widgets::{Block, Padding, Paragraph},
TerminalOptions, Viewport,
};
use crate::components::{BatchCommand, Command};
use super::{
create_dispatch,
spinner::{Spinner, SpinnerState},
Dispatch, IntoCommand, Msg, Receiver,
};
pub struct InlineCommand {
spinner: SpinnerState,
heading: String,
}
impl InlineCommand {
pub fn new(heading: impl Into<String>) -> Self {
Self {
spinner: SpinnerState::default(),
heading: heading.into(),
}
}
pub async fn execute<F, Fut>(&mut self, func: F) -> anyhow::Result<()>
where
F: FnOnce() -> Fut + Send + Sync + 'static,
Fut: futures::Future<Output = anyhow::Result<()>> + Send + 'static,
{
tracing::trace!("starting inline terminal");
let mut terminal = ratatui::init_with_options(TerminalOptions {
viewport: Viewport::Inline(3),
});
let (dispatch, mut receiver) = create_dispatch();
let mut event_stream = crossterm::event::EventStream::new();
let guard = TerminalGuard;
tokio::spawn({
let dispatch = dispatch.clone();
async move {
match func().await {
Ok(_) => dispatch.send(Msg::Success),
Err(e) => dispatch.send(Msg::Failure(e.to_string())),
}
}
});
loop {
if self
.update(&mut terminal, &dispatch, &mut receiver, &mut event_stream)
.await?
{
terminal.draw(|f| {
let buf = f.buffer_mut();
buf.reset();
})?;
break;
}
}
drop(guard);
Ok(())
}
async fn update(
&mut self,
terminal: &mut ratatui::Terminal<impl Backend>,
dispatch: &Dispatch,
receiver: &mut Receiver,
event_stream: &mut EventStream,
) -> anyhow::Result<bool> {
let input_event = event_stream.next().fuse();
let next_msg = receiver.next().fuse();
const FRAMES_PER_SECOND: f32 = 60.0;
const TICK_RATE: f32 = 20.0;
let period_frame = Duration::from_secs_f32(1.0 / FRAMES_PER_SECOND);
let mut interval_frames = tokio::time::interval(period_frame);
let period_tick = Duration::from_secs_f32(1.0 / TICK_RATE);
let mut interval_ticks = tokio::time::interval(period_tick);
let msg = tokio::select! {
_ = interval_frames.tick() => {
terminal.draw(|frame| self.draw(frame))?;
None
}
_ = interval_ticks.tick() => {
Some(Msg::Tick)
}
msg = next_msg => {
msg
}
input = input_event => {
if let Some(Ok(input)) = input {
self.handle_key_event(input)
} else {
None
}
}
};
if let Some(msg) = msg {
if Msg::Quit == msg {
return Ok(true);
}
let mut cmd = self.update_state(&msg);
loop {
let msg = cmd.into_command().execute(dispatch);
match msg {
Some(Msg::Quit) => return Ok(true),
Some(msg) => {
cmd = self.update_state(&msg);
}
None => break,
}
}
}
Ok(false)
}
fn draw(&mut self, frame: &mut Frame<'_>) {
let spinner = Spinner::new(Span::from(&self.heading));
let block = Block::new().padding(Padding::symmetric(2, 1));
StatefulWidget::render(
spinner.block(block),
frame.area(),
frame.buffer_mut(),
&mut self.spinner,
);
}
fn handle_key_event(&mut self, event: crossterm::event::Event) -> Option<Msg> {
if let crossterm::event::Event::Key(key) = event {
return match key.code {
KeyCode::Esc => Some(Msg::Quit),
KeyCode::Char('c') => Some(Msg::Quit),
_ => None,
};
}
None
}
fn update_state(&mut self, msg: &Msg) -> impl IntoCommand {
tracing::debug!("handling message: {:?}", msg);
let mut batch = BatchCommand::default();
match msg {
Msg::Quit => {}
Msg::Tick => {}
Msg::Success => return Msg::Quit.into_command(),
Msg::Failure(f) => {
tracing::error!("command failed: {}", f);
return Msg::Quit.into_command();
}
}
batch.with(self.spinner.update(msg));
batch.into_command()
}
}
#[derive(Default)]
struct TerminalGuard;
impl Drop for TerminalGuard {
fn drop(&mut self) {
tracing::trace!("restoring inline terminal");
ratatui::restore();
}
}

View File

@ -1,92 +0,0 @@
use std::time::{Duration, Instant};
use ratatui::{
text::{Line, Span, Text},
widgets::{Block, Paragraph, StatefulWidget, Widget},
};
use super::{BatchCommand, Command, IntoCommand, Msg};
pub struct Spinner<'a> {
span: Span<'a>,
block: Option<Block<'a>>,
}
impl<'a> Spinner<'a> {
pub fn new(span: Span<'a>) -> Self {
Self { span, block: None }
}
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
}
impl<'a> StatefulWidget for Spinner<'a> {
type State = SpinnerState;
fn render(
self,
area: ratatui::prelude::Rect,
buf: &mut ratatui::prelude::Buffer,
state: &mut Self::State,
) {
let frame = MINIDOT_FRAMES
.get((state.frame) % MINIDOT_FRAMES.len())
.expect("to find a valid static frame");
let line = Line::from(vec![Span::from(*frame), Span::from(" "), self.span]);
let para = Paragraph::new(vec![line]);
let para = if let Some(block) = self.block {
para.block(block)
} else {
para
};
para.render(area, buf)
}
}
pub struct SpinnerState {
last_event: Instant,
interval: Duration,
frame: usize,
}
impl Default for SpinnerState {
fn default() -> Self {
Self {
last_event: Instant::now(),
interval: Duration::from_millis(1000 / 12),
frame: 0,
}
}
}
const MINIDOT_FRAMES: [&str; 10] = ["", "", "", "", "", "", "", "", "", ""];
impl SpinnerState {
pub fn update(&mut self, msg: &Msg) -> impl IntoCommand {
let mut batch = BatchCommand::default();
let now = Instant::now();
if now.duration_since(self.last_event) >= self.interval {
self.last_event = now;
self.next_state();
batch.with(Command::new(|d| {
d.send(Msg::Tick);
None
}));
}
batch
}
fn next_state(&mut self) {
self.frame = self.frame.wrapping_add(1);
}
}

View File

@ -1,6 +1,5 @@
use crate::{app::App, components::inline_command::InlineCommand, git_provider::Repository}; use crate::{app::App, git_provider::Repository};
#[derive(Debug, Clone)]
pub struct GitClone { pub struct GitClone {
app: &'static App, app: &'static App,
} }

View File

@ -5,7 +5,6 @@ use std::path::PathBuf;
use anyhow::Context; use anyhow::Context;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use commands::root::RootCommand; use commands::root::RootCommand;
use components::inline_command::InlineCommand;
use config::Config; use config::Config;
use tracing::level_filters::LevelFilter; use tracing::level_filters::LevelFilter;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
@ -14,7 +13,6 @@ mod app;
mod cache; mod cache;
mod cache_codec; mod cache_codec;
mod commands; mod commands;
mod components;
mod config; mod config;
mod fuzzy_matcher; mod fuzzy_matcher;
mod git_clone; mod git_clone;
@ -58,7 +56,7 @@ async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_env_filter( .with_env_filter(
EnvFilter::builder() EnvFilter::builder()
.with_default_directive(LevelFilter::ERROR.into()) .with_default_directive(LevelFilter::WARN.into())
.from_env_lossy(), .from_env_lossy(),
) )
.init(); .init();