Skip to content

Commit 38ec69b

Browse files
committed
feat(tui): initialize with Ratatui
1 parent 0f653bf commit 38ec69b

19 files changed

+2162
-309
lines changed

Cargo.lock

+1,187-197
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[workspace]
22
resolver = "2"
3-
members = ["git-cliff-core", "git-cliff"]
3+
members = ["git-cliff-core", "git-cliff", "git-cliff-tui"]
44

55
[workspace.dependencies]
66
regex = "1.11.1"

git-cliff-core/src/changelog.rs

+11-9
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,21 @@ use std::time::{
2828
};
2929

3030
/// Changelog generator.
31-
#[derive(Debug)]
31+
#[derive(Clone, Debug)]
3232
pub struct Changelog<'a> {
3333
/// Releases that the changelog will contain.
3434
pub releases: Vec<Release<'a>>,
35+
/// Configuration.
36+
pub config: Config,
3537
header_template: Option<Template>,
3638
body_template: Template,
3739
footer_template: Option<Template>,
38-
config: &'a Config,
3940
additional_context: HashMap<String, serde_json::Value>,
4041
}
4142

4243
impl<'a> Changelog<'a> {
4344
/// Constructs a new instance.
44-
pub fn new(releases: Vec<Release<'a>>, config: &'a Config) -> Result<Self> {
45+
pub fn new(releases: Vec<Release<'a>>, config: Config) -> Result<Self> {
4546
let mut changelog = Changelog::build(releases, config)?;
4647
changelog.add_remote_data()?;
4748
changelog.process_commits();
@@ -50,7 +51,7 @@ impl<'a> Changelog<'a> {
5051
}
5152

5253
/// Builds a changelog from releases and config.
53-
fn build(releases: Vec<Release<'a>>, config: &'a Config) -> Result<Self> {
54+
fn build(releases: Vec<Release<'a>>, config: Config) -> Result<Self> {
5455
let trim = config.changelog.trim.unwrap_or(true);
5556
Ok(Self {
5657
releases,
@@ -60,7 +61,7 @@ impl<'a> Changelog<'a> {
6061
}
6162
None => None,
6263
},
63-
body_template: get_body_template(config, trim)?,
64+
body_template: get_body_template(&config, trim)?,
6465
footer_template: match &config.changelog.footer {
6566
Some(footer) => {
6667
Some(Template::new("footer", footer.to_string(), trim)?)
@@ -73,7 +74,7 @@ impl<'a> Changelog<'a> {
7374
}
7475

7576
/// Constructs an instance from a serialized context object.
76-
pub fn from_context<R: Read>(input: &mut R, config: &'a Config) -> Result<Self> {
77+
pub fn from_context<R: Read>(input: &mut R, config: Config) -> Result<Self> {
7778
Changelog::build(serde_json::from_reader(input)?, config)
7879
}
7980

@@ -431,6 +432,7 @@ impl<'a> Changelog<'a> {
431432

432433
/// Adds information about the remote to the template context.
433434
pub fn add_remote_context(&mut self) -> Result<()> {
435+
debug!("Adding remote context...");
434436
self.additional_context.insert(
435437
"remote".to_string(),
436438
serde_json::to_value(self.config.remote.clone())?,
@@ -1028,7 +1030,7 @@ mod test {
10281030
#[test]
10291031
fn changelog_generator() -> Result<()> {
10301032
let (config, releases) = get_test_data();
1031-
let mut changelog = Changelog::new(releases, &config)?;
1033+
let mut changelog = Changelog::new(releases, config)?;
10321034
changelog.bump_version()?;
10331035
changelog.releases[0].timestamp = 0;
10341036
let mut out = Vec::new();
@@ -1147,7 +1149,7 @@ chore(deps): fix broken deps
11471149
",
11481150
),
11491151
));
1150-
let changelog = Changelog::new(releases, &config)?;
1152+
let changelog = Changelog::new(releases, config)?;
11511153
let mut out = Vec::new();
11521154
changelog.generate(&mut out)?;
11531155
assert_eq!(
@@ -1251,7 +1253,7 @@ chore(deps): fix broken deps
12511253
{% endfor %}{% endfor %}"#
12521254
.to_string(),
12531255
);
1254-
let mut changelog = Changelog::new(releases, &config)?;
1256+
let mut changelog = Changelog::new(releases, config)?;
12551257
changelog.add_context("custom_field", "Hello")?;
12561258
let mut out = Vec::new();
12571259
changelog.generate(&mut out)?;

git-cliff-core/src/repo.rs

+9-9
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,6 @@ impl Repository {
7575
}
7676
}
7777

78-
/// Returns the path of the repository.
79-
pub fn path(&self) -> PathBuf {
80-
let mut path = self.inner.path().to_path_buf();
81-
if path.ends_with(".git") {
82-
path.pop();
83-
}
84-
path
85-
}
86-
8778
/// Sets the range for the commit search.
8879
///
8980
/// When a single SHA is provided as the range, start from the
@@ -104,6 +95,15 @@ impl Repository {
10495
Ok(())
10596
}
10697

98+
/// Returns the path of the repository.
99+
pub fn path(&self) -> PathBuf {
100+
let mut path = self.inner.path().to_path_buf();
101+
if path.ends_with(".git") {
102+
path.pop();
103+
}
104+
path
105+
}
106+
107107
/// Parses and returns the commits.
108108
///
109109
/// Sorts the commits by their time.

git-cliff-core/src/template.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use tera::{
2020
};
2121

2222
/// Wrapper for [`Tera`].
23-
#[derive(Debug)]
23+
#[derive(Clone, Debug)]
2424
pub struct Template {
2525
/// Template name.
2626
name: String,

git-cliff-tui/Cargo.toml

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "git-cliff-tui"
3+
version = "2.5.0" # managed by release.sh
4+
authors = ["Orhun Parmaksız <[email protected]>"]
5+
license = "MIT"
6+
edition = "2021"
7+
8+
[dependencies]
9+
ratatui = { version = "0.29.0", features = ["unstable-widget-ref"] }
10+
copypasta = "0.10.1"
11+
tui-markdown = "0.3.0"
12+
ansi-to-tui = "7.0.0"
13+
unicode-width = "0.2.0"
14+
tachyonfx = "0.10.1"
15+
lazy_static.workspace = true
16+
17+
[dependencies.git-cliff]
18+
version = "2.5.0" # managed by release.sh
19+
path = "../git-cliff"
20+
default-features = false
21+
features = ["github", "gitlab", "gitea", "bitbucket"]

git-cliff-tui/src/effect.rs

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
use std::time::Instant;
2+
3+
use ratatui::{
4+
layout::Rect,
5+
style::Color,
6+
};
7+
use tachyonfx::{
8+
fx,
9+
Effect,
10+
HslConvertable,
11+
};
12+
13+
use tachyonfx::Interpolatable;
14+
15+
pub trait IndexResolver<T: Clone> {
16+
fn resolve(idx: usize, data: &[T]) -> &T;
17+
}
18+
19+
#[derive(Clone, Debug)]
20+
pub struct ColorCycle<T: IndexResolver<Color>> {
21+
colors: Vec<Color>,
22+
_marker: std::marker::PhantomData<T>,
23+
}
24+
25+
#[derive(Clone, Debug)]
26+
pub struct PingPongCycle;
27+
28+
impl IndexResolver<Color> for PingPongCycle {
29+
fn resolve(idx: usize, data: &[Color]) -> &Color {
30+
let dbl_idx = idx % (2 * data.len());
31+
let final_index = if dbl_idx < data.len() {
32+
dbl_idx
33+
} else {
34+
2 * data.len() - 1 - dbl_idx
35+
};
36+
37+
data.get(final_index)
38+
.expect("ColorCycle: index out of bounds")
39+
}
40+
}
41+
42+
pub type PingPongColorCycle = ColorCycle<PingPongCycle>;
43+
44+
#[derive(Clone, Debug)]
45+
pub struct RepeatingCycle;
46+
47+
impl IndexResolver<Color> for RepeatingCycle {
48+
fn resolve(idx: usize, data: &[Color]) -> &Color {
49+
data.get(idx % data.len())
50+
.expect("ColorCycle: index out of bounds")
51+
}
52+
}
53+
54+
pub type RepeatingColorCycle = ColorCycle<RepeatingCycle>;
55+
56+
impl<T> ColorCycle<T>
57+
where
58+
T: IndexResolver<Color>,
59+
{
60+
pub fn new(initial_color: Color, colors: &[(usize, Color)]) -> Self {
61+
let mut gradient = vec![initial_color];
62+
colors
63+
.iter()
64+
.fold((0, initial_color), |(_, prev_color), (len, color)| {
65+
(0..=*len).for_each(|i| {
66+
let color = prev_color.lerp(color, i as f32 / *len as f32);
67+
gradient.push(color);
68+
});
69+
gradient.push(*color);
70+
(*len, *color)
71+
});
72+
73+
Self {
74+
colors: gradient,
75+
_marker: std::marker::PhantomData,
76+
}
77+
}
78+
79+
pub fn color_at(&self, idx: usize) -> &Color {
80+
T::resolve(idx, &self.colors)
81+
}
82+
}
83+
84+
/// Creates a repeating color cycle based on a base color.
85+
///
86+
/// # Arguments
87+
/// * `base_color` - Primary color to derive the cycle from
88+
/// * `length_multiplier` - Factor to adjust the cycle length
89+
///
90+
/// # Returns
91+
/// A ColorCycle instance with derived colors and adjusted steps.
92+
fn create_color_cycle(
93+
base_color: Color,
94+
length_multiplier: usize,
95+
) -> ColorCycle<RepeatingCycle> {
96+
let color_step: usize = 7 * length_multiplier;
97+
98+
let (h, s, l) = base_color.to_hsl();
99+
100+
let color_l = Color::from_hsl(h, s, 80.0);
101+
let color_d = Color::from_hsl(h, s, 40.0);
102+
103+
RepeatingColorCycle::new(base_color, &[
104+
(4 * length_multiplier, color_d),
105+
(2 * length_multiplier, color_l),
106+
(
107+
4 * length_multiplier,
108+
Color::from_hsl((h - 25.0) % 360.0, s, (l + 10.0).min(100.0)),
109+
),
110+
(
111+
color_step,
112+
Color::from_hsl(h, (s - 20.0).max(0.0), (l + 10.0).min(100.0)),
113+
),
114+
(
115+
color_step,
116+
Color::from_hsl((h + 25.0) % 360.0, s, (l + 10.0).min(100.0)),
117+
),
118+
(
119+
color_step,
120+
Color::from_hsl(h, (s + 20.0).max(0.0), (l + 10.0).min(100.0)),
121+
),
122+
])
123+
}
124+
125+
/// Creates an animated border effect using color cycling.
126+
///
127+
/// # Arguments
128+
/// * `base_color` - The primary color to base the cycling effect on
129+
/// * `area` - The rectangular area where the effect should be rendered
130+
///
131+
/// # Returns
132+
///
133+
/// An Effect that animates a border around the specified area using cycled
134+
/// colors
135+
pub fn create_border_effect(
136+
base_color: Color,
137+
speed: f32,
138+
length: usize,
139+
area: Rect,
140+
) -> Effect {
141+
let color_cycle = create_color_cycle(base_color, length);
142+
143+
let effect =
144+
fx::effect_fn_buf(Instant::now(), u32::MAX, move |started_at, ctx, buf| {
145+
let elapsed = started_at.elapsed().as_secs_f32();
146+
147+
// speed n cells/s
148+
let idx = (elapsed * speed) as usize;
149+
150+
let area = ctx.area;
151+
152+
let mut update_cell = |(x, y): (u16, u16), idx: usize| {
153+
if let Some(cell) = buf.cell_mut((x, y)) {
154+
cell.set_fg(*color_cycle.color_at(idx));
155+
}
156+
};
157+
158+
(area.x..area.right()).enumerate().for_each(|(i, x)| {
159+
update_cell((x, area.y), idx + i);
160+
});
161+
162+
let cell_idx_offset = area.width as usize;
163+
(area.y + 1..area.bottom() - 1)
164+
.enumerate()
165+
.for_each(|(i, y)| {
166+
update_cell((area.right() - 1, y), idx + i + cell_idx_offset);
167+
});
168+
169+
let cell_idx_offset =
170+
cell_idx_offset + area.height.saturating_sub(2) as usize;
171+
(area.x..area.right()).rev().enumerate().for_each(|(i, x)| {
172+
update_cell((x, area.bottom() - 1), idx + i + cell_idx_offset);
173+
});
174+
175+
let cell_idx_offset = cell_idx_offset + area.width as usize;
176+
(area.y + 1..area.bottom())
177+
.rev()
178+
.enumerate()
179+
.for_each(|(i, y)| {
180+
update_cell((area.x, y), idx + i + cell_idx_offset);
181+
});
182+
});
183+
184+
effect.with_area(area)
185+
}

0 commit comments

Comments
 (0)