gitkraft_core/utils/
mod.rs1pub mod text;
4pub mod time;
5
6pub use text::truncate_str;
7pub use time::{fmt_oid, relative_time, short_oid, short_oid_str};
8
9pub fn ascending_range(anchor: usize, target: usize) -> Vec<usize> {
21 let (start, end) = if anchor <= target {
22 (anchor, target)
23 } else {
24 (target, anchor)
25 };
26 (start..=end).collect()
27}
28
29pub fn wrap_next(current: usize, len: usize) -> usize {
32 if len == 0 {
33 return 0;
34 }
35 (current + 1) % len
36}
37
38pub fn wrap_prev(current: usize, len: usize) -> usize {
41 if len == 0 {
42 return 0;
43 }
44 if current == 0 {
45 len - 1
46 } else {
47 current - 1
48 }
49}
50
51pub fn clamp_next(current: usize, len: usize) -> usize {
54 if len == 0 {
55 return 0;
56 }
57 (current + 1).min(len - 1)
58}
59
60pub fn repo_display_name(path: Option<&std::path::Path>) -> String {
63 path.and_then(|p| p.file_name())
64 .map(|n| n.to_string_lossy().into_owned())
65 .unwrap_or_else(|| "New Tab".into())
66}
67
68pub fn clamp_selection(current: Option<usize>, len: usize) -> Option<usize> {
74 if len == 0 {
75 return None;
76 }
77 Some(current.unwrap_or(0).min(len - 1))
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn ascending_range_low_to_high() {
86 assert_eq!(ascending_range(2, 5), vec![2, 3, 4, 5]);
87 }
88
89 #[test]
90 fn ascending_range_high_to_low_is_still_ascending() {
91 assert_eq!(ascending_range(5, 2), vec![2, 3, 4, 5]);
92 }
93
94 #[test]
95 fn ascending_range_same_returns_single() {
96 assert_eq!(ascending_range(4, 4), vec![4]);
97 }
98
99 #[test]
102 fn wrap_next_normal() {
103 assert_eq!(wrap_next(0, 3), 1);
104 }
105
106 #[test]
107 fn wrap_next_wraps_around() {
108 assert_eq!(wrap_next(2, 3), 0);
109 }
110
111 #[test]
112 fn wrap_next_empty_list() {
113 assert_eq!(wrap_next(0, 0), 0);
114 }
115
116 #[test]
117 fn wrap_next_single_element() {
118 assert_eq!(wrap_next(0, 1), 0);
119 }
120
121 #[test]
124 fn wrap_prev_normal() {
125 assert_eq!(wrap_prev(2, 3), 1);
126 }
127
128 #[test]
129 fn wrap_prev_wraps_around() {
130 assert_eq!(wrap_prev(0, 3), 2);
131 }
132
133 #[test]
134 fn wrap_prev_empty_list() {
135 assert_eq!(wrap_prev(0, 0), 0);
136 }
137
138 #[test]
139 fn wrap_prev_single_element() {
140 assert_eq!(wrap_prev(0, 1), 0);
141 }
142
143 #[test]
146 fn clamp_next_normal() {
147 assert_eq!(clamp_next(0, 3), 1);
148 }
149
150 #[test]
151 fn clamp_next_at_end() {
152 assert_eq!(clamp_next(2, 3), 2);
153 }
154
155 #[test]
156 fn clamp_next_empty_list() {
157 assert_eq!(clamp_next(0, 0), 0);
158 }
159
160 #[test]
163 fn clamp_selection_empty_list() {
164 assert_eq!(clamp_selection(Some(5), 0), None);
165 }
166
167 #[test]
168 fn clamp_selection_none_becomes_first() {
169 assert_eq!(clamp_selection(None, 3), Some(0));
170 }
171
172 #[test]
173 fn clamp_selection_within_range() {
174 assert_eq!(clamp_selection(Some(1), 3), Some(1));
175 }
176
177 #[test]
178 fn clamp_selection_overflow_clamped() {
179 assert_eq!(clamp_selection(Some(10), 3), Some(2));
180 }
181
182 #[test]
185 fn repo_display_name_with_path() {
186 let p = std::path::Path::new("/home/user/my-project");
187 assert_eq!(repo_display_name(Some(p)), "my-project");
188 }
189
190 #[test]
191 fn repo_display_name_none() {
192 assert_eq!(repo_display_name(None), "New Tab");
193 }
194
195 #[test]
196 fn repo_display_name_root_path() {
197 let p = std::path::Path::new("/");
198 assert_eq!(repo_display_name(Some(p)), "New Tab");
199 }
200}