2 Commits

Author SHA1 Message Date
forust 549025dfe7 chore: release v1.2.1
release / verify (push) Successful in 25s
ci / rust (push) Successful in 48s
ci / lint-yaml (push) Successful in 4s
release / linux-amd64 (push) Successful in 30s
2026-07-05 02:53:49 +02:00
forust fface10081 feat: use large timer display
ci / rust (push) Successful in 44s
ci / lint-yaml (push) Successful in 5s
2026-07-05 02:51:26 +02:00
5 changed files with 159 additions and 36 deletions
+7
View File
@@ -4,6 +4,12 @@ All notable changes to this project are documented here.
The project uses semantic versioning. Tags are formatted as `vMAJOR.MINOR.PATCH`. The project uses semantic versioning. Tags are formatted as `vMAJOR.MINOR.PATCH`.
## [1.2.1] - 2026-07-05
### Changed
- Updated the TUI running screen to center a larger termdown-style ASCII time-left display above progress and post-countdown commands.
## [1.2.0] - 2026-07-05 ## [1.2.0] - 2026-07-05
### Added ### Added
@@ -42,6 +48,7 @@ The project uses semantic versioning. Tags are formatted as `vMAJOR.MINOR.PATCH`
- Added YAML configuration, CLI `init`, `preview`, and `run` commands. - Added YAML configuration, CLI `init`, `preview`, and `run` commands.
- Added Gitea CI and tag-based release workflows. - Added Gitea CI and tag-based release workflows.
[1.2.1]: https://gitea.forust.xyz/forust/gosleep/releases/tag/v1.2.1
[1.2.0]: https://gitea.forust.xyz/forust/gosleep/releases/tag/v1.2.0 [1.2.0]: https://gitea.forust.xyz/forust/gosleep/releases/tag/v1.2.0
[1.1.3]: https://gitea.forust.xyz/forust/gosleep/releases/tag/v1.1.3 [1.1.3]: https://gitea.forust.xyz/forust/gosleep/releases/tag/v1.1.3
[1.1.2]: https://gitea.forust.xyz/forust/gosleep/releases/tag/v1.1.2 [1.1.2]: https://gitea.forust.xyz/forust/gosleep/releases/tag/v1.1.2
Generated
+1 -1
View File
@@ -298,7 +298,7 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]] [[package]]
name = "gosleep-timer" name = "gosleep-timer"
version = "1.2.0" version = "1.2.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "gosleep-timer" name = "gosleep-timer"
version = "1.2.0" version = "1.2.1"
edition = "2024" edition = "2024"
rust-version = "1.85" rust-version = "1.85"
description = "Terminal sleep timer for Linux desktop actions, with YAML config and a Rust TUI" description = "Terminal sleep timer for Linux desktop actions, with YAML config and a Rust TUI"
+1 -1
View File
@@ -1 +1 @@
1.2.0 1.2.1
+149 -33
View File
@@ -993,35 +993,33 @@ impl App {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([ .constraints([
Constraint::Min(1),
Constraint::Length(9),
Constraint::Min(1),
Constraint::Length(2), Constraint::Length(2),
Constraint::Length(7), Constraint::Length(8),
Constraint::Length(2),
Constraint::Min(4),
Constraint::Length(1), Constraint::Length(1),
]) ])
.split(area); .split(area);
frame.render_widget(
Paragraph::new("gosleep-timer").style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
chunks[0],
);
let time_lines = ascii_time(&format_compact_duration(remaining)) let time_lines = ascii_time(&format_compact_duration(remaining))
.into_iter() .into_iter()
.map(|line| Line::from(Span::styled(line, Style::default().fg(Color::Cyan)))) .map(|line| Line::from(Span::styled(line, Style::default().fg(Color::White))))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
frame.render_widget(Paragraph::new(time_lines), chunks[1]); let time_width = time_lines
.iter()
.map(|line| line.width() as u16)
.max()
.unwrap_or(0);
let timer_area = center_rect(time_width.min(chunks[1].width), 7, chunks[1]);
frame.render_widget(Paragraph::new(time_lines), timer_area);
frame.render_widget( frame.render_widget(
Gauge::default() Gauge::default()
.gauge_style(Style::default().fg(Color::Magenta)) .gauge_style(Style::default().fg(Color::Magenta))
.label(format_gauge_label(progress)) .label(format_gauge_label(progress))
.ratio(progress), .ratio(progress),
chunks[2], chunks[3],
); );
let commands = build_action_commands(&self.config); let commands = build_action_commands(&self.config);
@@ -1038,7 +1036,7 @@ impl App {
}; };
wrap_command( wrap_command(
&command.display(), &command.display(),
chunks[3].width.saturating_sub(4) as usize, chunks[4].width.saturating_sub(4) as usize,
) )
.into_iter() .into_iter()
.map(move |line| Line::from(Span::styled(line, style))) .map(move |line| Line::from(Span::styled(line, style)))
@@ -1054,7 +1052,7 @@ impl App {
.border_style(Color::Blue), .border_style(Color::Blue),
) )
.wrap(Wrap { trim: false }), .wrap(Wrap { trim: false }),
chunks[3], chunks[4],
); );
let footer = if self let footer = if self
@@ -1068,7 +1066,7 @@ impl App {
}; };
frame.render_widget( frame.render_widget(
Paragraph::new(footer).style(Style::default().fg(Color::DarkGray)), Paragraph::new(footer).style(Style::default().fg(Color::DarkGray)),
chunks[4], chunks[5],
); );
} }
@@ -1671,33 +1669,141 @@ fn format_gauge_label(progress: f64) -> String {
} }
fn ascii_time(value: &str) -> Vec<String> { fn ascii_time(value: &str) -> Vec<String> {
let mut rows = vec![String::new(); 5]; const GLYPH_WIDTH: usize = 10;
let mut rows = vec![String::new(); 7];
for ch in value.chars() { for ch in value.chars() {
let glyph = ascii_glyph(ch); let glyph = ascii_glyph(ch);
for (row, part) in rows.iter_mut().zip(glyph) { for (row, part) in rows.iter_mut().zip(glyph) {
if !row.is_empty() { if !row.is_empty() {
row.push(' '); row.push(' ');
} }
row.push_str(part); row.push_str(&format!("{part:<GLYPH_WIDTH$}"));
} }
} }
rows rows
} }
fn ascii_glyph(ch: char) -> [&'static str; 5] { fn ascii_glyph(ch: char) -> [&'static str; 7] {
match ch { match ch {
'0' => [" ### ", "# #", "# #", "# #", " ### "], '0' => [
'1' => [" # ", " ## ", " # ", " # ", " ### "], " ad8888ba ",
'2' => [" ### ", "# #", " # ", " # ", "#####"], "d8\" \"8b",
'3' => ["#### ", " #", " ### ", " #", "#### "], "8P 88",
'4' => ["# #", "# #", "#####", " #", " #"], "8b d8",
'5' => ["#####", "# ", "#### ", " #", "#### "], "8b d8",
'6' => [" ### ", "# ", "#### ", "# #", " ### "], "Y8a. .a8P",
'7' => ["#####", " #", " # ", " # ", " # "], " \"Y8888Y\" ",
'8' => [" ### ", "# #", " ### ", "# #", " ### "], ],
'9' => [" ### ", "# #", " ####", " #", " ### "], '1' => [
':' => [" ", " # ", " ", " # ", " "], " 88 ",
_ => [" ", " ", " ", " ", " "], " 888 ",
" 88 ",
" 88 ",
" 88 ",
" 88 ",
" 888888 ",
],
'2' => [
" ad8888ba ",
"d8\" \"8b",
" ,8P",
" ,d8P ",
" ,d8P' ",
" d8P' ",
"888888888",
],
'3' => [
" ad8888ba ",
"d8\" \"8b",
" ,8P",
" aad8\" ",
" `8b",
"Y8a. .a8P",
" \"Y8888Y\" ",
],
'4' => [
"88 88",
"88 88",
"88 88",
"888888888",
" 88",
" 88",
" 88",
],
'5' => [
"888888888",
"88 ",
"88 ",
"888888ba ",
" 8b",
"Y8a. .a8P",
" \"Y8888Y\" ",
],
'6' => [
" ad8888ba ",
"d8\" ",
"88 ",
"88PPPPba ",
"88 8b",
"Y8a. .a8P",
" \"Y8888Y\" ",
],
'7' => [
"888888888",
" ,8P",
" ,8P ",
" ,8P ",
" ,8P ",
" ,8P ",
" ,8P ",
],
'8' => [
" ad8888ba ",
"d8\" \"8b",
"Y8a. .a8P",
" Y8888P ",
"d8\" \"8b",
"Y8a. .a8P",
" \"Y8888Y\" ",
],
'9' => [
" ad8888ba ",
"d8\" \"8b",
"Y8a. .a8P",
" \"Y888888",
" 88",
"Y8a. .a8P",
" \"Y8888Y\" ",
],
':' => [
" ",
" 88 ",
" 88 ",
" ",
" 88 ",
" 88 ",
" ",
],
_ => [
" ",
" ",
" ",
" ",
" ",
" ",
" ",
],
}
}
fn center_rect(width: u16, height: u16, area: Rect) -> Rect {
let width = width.min(area.width);
let height = height.min(area.height);
Rect {
x: area.x + area.width.saturating_sub(width) / 2,
y: area.y + area.height.saturating_sub(height) / 2,
width,
height,
} }
} }
@@ -1939,6 +2045,16 @@ mod tests {
assert!(keys.contains(&FieldKey::CustomCommands)); assert!(keys.contains(&FieldKey::CustomCommands));
} }
#[test]
fn ascii_time_rows_have_equal_width() {
let rows = ascii_time("14:45:27");
let width = rows.first().map(String::len).unwrap_or(0);
assert_eq!(rows.len(), 7);
assert!(width > 0);
assert!(rows.iter().all(|row| row.len() == width));
}
fn visible_field_keys(config: &Config) -> Vec<FieldKey> { fn visible_field_keys(config: &Config) -> Vec<FieldKey> {
visible_fields(config) visible_fields(config)
.into_iter() .into_iter()