=== fossil info ===
status: ok
stdout:
project-name: lazyfossil
repository: /home/geraldo/museum/lazyfossil.fossil
local-root: /home/geraldo/Documents/pi_lazyfossil/
config-db: /home/geraldo/.config/fossil.db
project-code: 3c93e1c05dd9744de1e9512b949b2d45d8052f60
checkout: bab3996240954064d8ad695417e2d1f6b7d4bbdb 2026-06-06 05:02:24 UTC
parent: 760df322ef53785be6eac26db6e1fa9470a1ad3d 2026-06-05 20:06:18 UTC
tags: trunk
comment: Add publish github release workflow (user: geraldo)
check-ins: 24
=== fossil status ===
status: ok
stdout:
repository: /home/geraldo/museum/lazyfossil.fossil
local-root: /home/geraldo/Documents/pi_lazyfossil/
config-db: /home/geraldo/.config/fossil.db
checkout: bab3996240954064d8ad695417e2d1f6b7d4bbdb 2026-06-06 05:02:24 UTC
parent: 760df322ef53785be6eac26db6e1fa9470a1ad3d 2026-06-05 20:06:18 UTC
tags: trunk
comment: Add publish github release workflow (user: geraldo)
ADDED Makefile
EDITED README.md
ADDED doc/images/lazyfossil_logo.png
ADDED doc/images/lazyfossil_logo_01.txt
ADDED doc/images/lazyfossil_logo_02.txt
ADDED doc/images/lazyfossil_logo_03.txt
ADDED doc/images/lazyfossil_logo_high_res.png
EDITED src/app.rs
EDITED src/fossil.rs
EDITED src/ui.rs
stderr:
setting ignore-glob has both versioned and non-versioned values: using versioned value from file "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob" (to silence this warning, either create an empty file named "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob.no-warn" in the check-out root, or delete the non-versioned setting with "fossil unset ignore-glob")
=== fossil extras --dotfiles ===
status: ok
stdout:
.pi/plans/2026-06-03-fossil-tui-mvp.md
.pi/plans/2026-06-03-rethink-commit-selection.md
.pi/plans/2026-06-04-fossil-tui-mvp.md
README.bak
crates.io-athena-token
doc/images/lazyfossil_logo_2_high_res.png
stderr:
setting ignore-glob has both versioned and non-versioned values: using versioned value from file "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob" (to silence this warning, either create an empty file named "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob.no-warn" in the check-out root, or delete the non-versioned setting with "fossil unset ignore-glob")
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c ===
status: ok
stdout:
bab3996240|geraldo|2026-06-06 05:02:24|Add publish github release workflow
760df322ef|geraldo|2026-06-05 20:06:18|Add badge to README
1ac6b345e4|geraldo|2026-06-05 20:05:30|Fix github workflow binary path
67774b0a64|geraldo|2026-06-05 19:05:41|fix github workflow - binary_name
e507e22d1b|geraldo|2026-06-05 18:50:53|Bump version to 0.3.1 and rebuild
de8e50f7b0|geraldo|2026-06-05 18:25:33|Add github workflow
dbe9c779db|geraldo|2026-06-05 18:12:57|Update repository to github
d82a046b8e|geraldo|2026-06-05 18:08:41|Mirror fossil repository to github
14b139337f|geraldo|2026-06-05 18:06:58|Include hidden files in extra listing
3e4f100ab4|geraldo|2026-06-05 18:03:02|Add sync feature
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0bc11330ff|geraldo|2026-06-04 18:02:47|Add test cases
0b5ee26b73|geraldo|2026-06-03 20:21:03|Update README
1094a55d21|geraldo|2026-06-03 20:10:05|Implement commit selection flow and bump version to 0.1.3
daa11aa0c1|geraldo|2026-06-03 20:07:51|Add README
b66b54d85b|geraldo|2026-06-03 20:04:50|Update version in Cargo.lock
f6f5577549|geraldo|2026-06-03 19:48:00|Bump version to 0.1.2 and save current TUI progress
504d232dbd|geraldo|2026-06-03 18:40:24|Preview extra files and bump version to 0.1.1
9847421853|geraldo|2026-06-02 20:15:33|Prepare crates.io metadata and docs
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p Makefile ===
status: ok
stdout:
=== fossil diff -- Makefile ===
status: ok
stdout:
ADDED Makefile
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p README.md ===
status: ok
stdout:
760df322ef|geraldo|2026-06-05 20:06:18|Add badge to README
e507e22d1b|geraldo|2026-06-05 18:50:53|Bump version to 0.3.1 and rebuild
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0b5ee26b73|geraldo|2026-06-03 20:21:03|Update README
daa11aa0c1|geraldo|2026-06-03 20:07:51|Add README
=== fossil diff -- README.md ===
status: ok
stdout:
Index: README.md
==================================================================
@@ -1,15 +1,12 @@
# lazyfossil
[](https://github.com/geraldolsribeiro/lazyfossil/actions/workflows/release.yml)
-A lazygit-inspired terminal UI for Fossil SCM.
+
-## Versioning
-
-This project follows semantic versioning: `MAJOR.MINOR.PATCH`.
-Current version: `0.3.1`.
+A lazygit-inspired terminal UI for Fossil SCM.
## Project goals
- Fast terminal workflow for Fossil checkouts
- Working tree and history browsing in one UI
@@ -59,5 +56,11 @@
## Run
```bash
cargo run
```
+
+## Versioning
+
+This project follows semantic versioning: `MAJOR.MINOR.PATCH`.
+Current version: `0.3.1`.
+
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p doc/images/lazyfossil_logo.png ===
status: ok
stdout:
=== fossil diff -- doc/images/lazyfossil_logo.png ===
status: ok
stdout:
ADDED doc/images/lazyfossil_logo.png
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p doc/images/lazyfossil_logo_01.txt ===
status: ok
stdout:
=== fossil diff -- doc/images/lazyfossil_logo_01.txt ===
status: ok
stdout:
ADDED doc/images/lazyfossil_logo_01.txt
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p doc/images/lazyfossil_logo_02.txt ===
status: ok
stdout:
=== fossil diff -- doc/images/lazyfossil_logo_02.txt ===
status: ok
stdout:
ADDED doc/images/lazyfossil_logo_02.txt
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p doc/images/lazyfossil_logo_03.txt ===
status: ok
stdout:
=== fossil diff -- doc/images/lazyfossil_logo_03.txt ===
status: ok
stdout:
ADDED doc/images/lazyfossil_logo_03.txt
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p doc/images/lazyfossil_logo_high_res.png ===
status: ok
stdout:
=== fossil diff -- doc/images/lazyfossil_logo_high_res.png ===
status: ok
stdout:
ADDED doc/images/lazyfossil_logo_high_res.png
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p doc/images/lazyfossil_logo_03.txt ===
status: ok
stdout:
=== fossil diff -- doc/images/lazyfossil_logo_03.txt ===
status: ok
stdout:
ADDED doc/images/lazyfossil_logo_03.txt
=== fossil commit -m Add project logo Makefile README.md doc/images/lazyfossil_logo.png doc/images/lazyfossil_logo_01.txt doc/images/lazyfossil_logo_02.txt doc/images/lazyfossil_logo_03.txt doc/images/lazyfossil_logo_high_res.png ===
status: err
stdout:
Pull from https://geraldo@intmain.dev/museum/lazyfossil
Round-trips: 1 Artifacts sent: 0 received: 0
Pull done, wire bytes sent: 496 received: 553 remote: 213.238.180.157
./doc/images/lazyfossil_logo.png contains binary data. Use --no-warnings or the "binary-glob" setting to disable this warning.
Commit anyhow (a=all/y/N)?
stderr:
Abandoning commit due to binary data in ./doc/images/lazyfossil_logo.png
=== fossil info ===
status: ok
stdout:
project-name: lazyfossil
repository: /home/geraldo/museum/lazyfossil.fossil
local-root: /home/geraldo/Documents/pi_lazyfossil/
config-db: /home/geraldo/.config/fossil.db
project-code: 3c93e1c05dd9744de1e9512b949b2d45d8052f60
checkout: bab3996240954064d8ad695417e2d1f6b7d4bbdb 2026-06-06 05:02:24 UTC
parent: 760df322ef53785be6eac26db6e1fa9470a1ad3d 2026-06-05 20:06:18 UTC
tags: trunk
comment: Add publish github release workflow (user: geraldo)
check-ins: 24
=== fossil status ===
status: ok
stdout:
repository: /home/geraldo/museum/lazyfossil.fossil
local-root: /home/geraldo/Documents/pi_lazyfossil/
config-db: /home/geraldo/.config/fossil.db
checkout: bab3996240954064d8ad695417e2d1f6b7d4bbdb 2026-06-06 05:02:24 UTC
parent: 760df322ef53785be6eac26db6e1fa9470a1ad3d 2026-06-05 20:06:18 UTC
tags: trunk
comment: Add publish github release workflow (user: geraldo)
ADDED Makefile
EDITED README.md
ADDED doc/images/lazyfossil_logo.png
ADDED doc/images/lazyfossil_logo_01.txt
ADDED doc/images/lazyfossil_logo_02.txt
ADDED doc/images/lazyfossil_logo_03.txt
ADDED doc/images/lazyfossil_logo_high_res.png
EDITED src/app.rs
EDITED src/fossil.rs
EDITED src/ui.rs
stderr:
setting ignore-glob has both versioned and non-versioned values: using versioned value from file "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob" (to silence this warning, either create an empty file named "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob.no-warn" in the check-out root, or delete the non-versioned setting with "fossil unset ignore-glob")
=== fossil extras --dotfiles ===
status: ok
stdout:
.pi/plans/2026-06-03-fossil-tui-mvp.md
.pi/plans/2026-06-03-rethink-commit-selection.md
.pi/plans/2026-06-04-fossil-tui-mvp.md
README.bak
crates.io-athena-token
doc/images/lazyfossil_logo_2_high_res.png
stderr:
setting ignore-glob has both versioned and non-versioned values: using versioned value from file "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob" (to silence this warning, either create an empty file named "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob.no-warn" in the check-out root, or delete the non-versioned setting with "fossil unset ignore-glob")
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c ===
status: ok
stdout:
bab3996240|geraldo|2026-06-06 05:02:24|Add publish github release workflow
760df322ef|geraldo|2026-06-05 20:06:18|Add badge to README
1ac6b345e4|geraldo|2026-06-05 20:05:30|Fix github workflow binary path
67774b0a64|geraldo|2026-06-05 19:05:41|fix github workflow - binary_name
e507e22d1b|geraldo|2026-06-05 18:50:53|Bump version to 0.3.1 and rebuild
de8e50f7b0|geraldo|2026-06-05 18:25:33|Add github workflow
dbe9c779db|geraldo|2026-06-05 18:12:57|Update repository to github
d82a046b8e|geraldo|2026-06-05 18:08:41|Mirror fossil repository to github
14b139337f|geraldo|2026-06-05 18:06:58|Include hidden files in extra listing
3e4f100ab4|geraldo|2026-06-05 18:03:02|Add sync feature
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0bc11330ff|geraldo|2026-06-04 18:02:47|Add test cases
0b5ee26b73|geraldo|2026-06-03 20:21:03|Update README
1094a55d21|geraldo|2026-06-03 20:10:05|Implement commit selection flow and bump version to 0.1.3
daa11aa0c1|geraldo|2026-06-03 20:07:51|Add README
b66b54d85b|geraldo|2026-06-03 20:04:50|Update version in Cargo.lock
f6f5577549|geraldo|2026-06-03 19:48:00|Bump version to 0.1.2 and save current TUI progress
504d232dbd|geraldo|2026-06-03 18:40:24|Preview extra files and bump version to 0.1.1
9847421853|geraldo|2026-06-02 20:15:33|Prepare crates.io metadata and docs
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p Makefile ===
status: ok
stdout:
=== fossil diff -- Makefile ===
status: ok
stdout:
ADDED Makefile
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p README.md ===
status: ok
stdout:
760df322ef|geraldo|2026-06-05 20:06:18|Add badge to README
e507e22d1b|geraldo|2026-06-05 18:50:53|Bump version to 0.3.1 and rebuild
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0b5ee26b73|geraldo|2026-06-03 20:21:03|Update README
daa11aa0c1|geraldo|2026-06-03 20:07:51|Add README
=== fossil diff -- README.md ===
status: ok
stdout:
Index: README.md
==================================================================
@@ -1,15 +1,12 @@
# lazyfossil
[](https://github.com/geraldolsribeiro/lazyfossil/actions/workflows/release.yml)
-A lazygit-inspired terminal UI for Fossil SCM.
+
-## Versioning
-
-This project follows semantic versioning: `MAJOR.MINOR.PATCH`.
-Current version: `0.3.1`.
+A lazygit-inspired terminal UI for Fossil SCM.
## Project goals
- Fast terminal workflow for Fossil checkouts
- Working tree and history browsing in one UI
@@ -59,5 +56,11 @@
## Run
```bash
cargo run
```
+
+## Versioning
+
+This project follows semantic versioning: `MAJOR.MINOR.PATCH`.
+Current version: `0.3.1`.
+
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p doc/images/lazyfossil_logo.png ===
status: ok
stdout:
=== fossil diff -- doc/images/lazyfossil_logo.png ===
status: ok
stdout:
ADDED doc/images/lazyfossil_logo.png
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p doc/images/lazyfossil_logo_01.txt ===
status: ok
stdout:
=== fossil diff -- doc/images/lazyfossil_logo_01.txt ===
status: ok
stdout:
ADDED doc/images/lazyfossil_logo_01.txt
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p doc/images/lazyfossil_logo_02.txt ===
status: ok
stdout:
=== fossil diff -- doc/images/lazyfossil_logo_02.txt ===
status: ok
stdout:
ADDED doc/images/lazyfossil_logo_02.txt
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p doc/images/lazyfossil_logo_03.txt ===
status: ok
stdout:
=== fossil diff -- doc/images/lazyfossil_logo_03.txt ===
status: ok
stdout:
ADDED doc/images/lazyfossil_logo_03.txt
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p doc/images/lazyfossil_logo_high_res.png ===
status: ok
stdout:
=== fossil diff -- doc/images/lazyfossil_logo_high_res.png ===
status: ok
stdout:
ADDED doc/images/lazyfossil_logo_high_res.png
=== fossil settings binary-glob *.png,*.jpg,*.jpeg,*.gif,*.ico ===
status: ok
=== fossil commit -m Add project logo Makefile README.md doc/images/lazyfossil_logo.png doc/images/lazyfossil_logo_01.txt doc/images/lazyfossil_logo_02.txt doc/images/lazyfossil_logo_03.txt doc/images/lazyfossil_logo_high_res.png ===
status: ok
stdout:
Pull from https://geraldo@intmain.dev/museum/lazyfossil
Round-trips: 1 Artifacts sent: 0 received: 0
Pull done, wire bytes sent: 494 received: 554 remote: 213.238.180.157
New_Version: fff8b35f249823910aab68967a8273dfa37daf6947f78a6c76274cfd1144e7a2
Sync with https://geraldo@intmain.dev/museum/lazyfossil
Round-trips: 2 Artifacts sent: 8 received: 0
Sync done, wire bytes sent: 1027468 received: 719 remote: 213.238.180.157
=== fossil info ===
status: ok
stdout:
project-name: lazyfossil
repository: /home/geraldo/museum/lazyfossil.fossil
local-root: /home/geraldo/Documents/pi_lazyfossil/
config-db: /home/geraldo/.config/fossil.db
project-code: 3c93e1c05dd9744de1e9512b949b2d45d8052f60
checkout: fff8b35f249823910aab68967a8273dfa37daf69 2026-06-06 07:13:07 UTC
parent: bab3996240954064d8ad695417e2d1f6b7d4bbdb 2026-06-06 05:02:24 UTC
tags: trunk
comment: Add project logo (user: geraldo)
check-ins: 25
=== fossil status ===
status: ok
stdout:
repository: /home/geraldo/museum/lazyfossil.fossil
local-root: /home/geraldo/Documents/pi_lazyfossil/
config-db: /home/geraldo/.config/fossil.db
checkout: fff8b35f249823910aab68967a8273dfa37daf69 2026-06-06 07:13:07 UTC
parent: bab3996240954064d8ad695417e2d1f6b7d4bbdb 2026-06-06 05:02:24 UTC
tags: trunk
comment: Add project logo (user: geraldo)
EDITED src/app.rs
EDITED src/fossil.rs
EDITED src/ui.rs
stderr:
setting ignore-glob has both versioned and non-versioned values: using versioned value from file "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob" (to silence this warning, either create an empty file named "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob.no-warn" in the check-out root, or delete the non-versioned setting with "fossil unset ignore-glob")
=== fossil extras --dotfiles ===
status: ok
stdout:
.pi/plans/2026-06-03-fossil-tui-mvp.md
.pi/plans/2026-06-03-rethink-commit-selection.md
.pi/plans/2026-06-04-fossil-tui-mvp.md
README.bak
crates.io-athena-token
doc/images/lazyfossil_logo_2_high_res.png
stderr:
setting ignore-glob has both versioned and non-versioned values: using versioned value from file "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob" (to silence this warning, either create an empty file named "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob.no-warn" in the check-out root, or delete the non-versioned setting with "fossil unset ignore-glob")
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c ===
status: ok
stdout:
fff8b35f24|geraldo|2026-06-06 07:13:07|Add project logo
bab3996240|geraldo|2026-06-06 05:02:24|Add publish github release workflow
760df322ef|geraldo|2026-06-05 20:06:18|Add badge to README
1ac6b345e4|geraldo|2026-06-05 20:05:30|Fix github workflow binary path
67774b0a64|geraldo|2026-06-05 19:05:41|fix github workflow - binary_name
e507e22d1b|geraldo|2026-06-05 18:50:53|Bump version to 0.3.1 and rebuild
de8e50f7b0|geraldo|2026-06-05 18:25:33|Add github workflow
dbe9c779db|geraldo|2026-06-05 18:12:57|Update repository to github
d82a046b8e|geraldo|2026-06-05 18:08:41|Mirror fossil repository to github
14b139337f|geraldo|2026-06-05 18:06:58|Include hidden files in extra listing
3e4f100ab4|geraldo|2026-06-05 18:03:02|Add sync feature
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0bc11330ff|geraldo|2026-06-04 18:02:47|Add test cases
0b5ee26b73|geraldo|2026-06-03 20:21:03|Update README
1094a55d21|geraldo|2026-06-03 20:10:05|Implement commit selection flow and bump version to 0.1.3
daa11aa0c1|geraldo|2026-06-03 20:07:51|Add README
b66b54d85b|geraldo|2026-06-03 20:04:50|Update version in Cargo.lock
f6f5577549|geraldo|2026-06-03 19:48:00|Bump version to 0.1.2 and save current TUI progress
504d232dbd|geraldo|2026-06-03 18:40:24|Preview extra files and bump version to 0.1.1
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p src/app.rs ===
status: ok
stdout:
3e4f100ab4|geraldo|2026-06-05 18:03:02|Add sync feature
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0bc11330ff|geraldo|2026-06-04 18:02:47|Add test cases
1094a55d21|geraldo|2026-06-03 20:10:05|Implement commit selection flow and bump version to 0.1.3
f6f5577549|geraldo|2026-06-03 19:48:00|Bump version to 0.1.2 and save current TUI progress
504d232dbd|geraldo|2026-06-03 18:40:24|Preview extra files and bump version to 0.1.1
05ae23bdc5|geraldo|2026-06-02 19:57:40|Add mouse file selection and diff scrolling
e7057da18b|geraldo|2026-06-02 19:52:08|Fix selected-file diff and footer status line
49410bfd5e|geraldo|2026-06-02 19:18:34|Initial lazyfossil TUI MVP
=== fossil diff -- src/app.rs ===
status: ok
stdout:
Index: src/app.rs
==================================================================
@@ -6,10 +6,13 @@
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use std::fs;
use std::io;
+use std::path::Path;
+
+const ASCII_LOGO: &str = include_str!("../doc/images/lazyfossil_logo_01.txt");
use std::time::Duration;
pub fn run() -> Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
@@ -115,14 +118,21 @@
fn refresh_diff(&mut self) {
if let Some(repo) = &self.state.repo {
if let Some(file) = repo.files.get(repo.selected_file) {
self.state.diff_scroll = 0;
self.state.diff = Some(match file.status.as_str() {
- "extra" => match fs::read_to_string(&file.path) {
- Ok(content) => {
- if content.trim().is_empty() { format!("Empty file: {}", file.path) } else { content }
- }
+ "extra" => match fs::read(&file.path) {
+ Ok(bytes) => match String::from_utf8(bytes) {
+ Ok(content) => {
+ if content.trim().is_empty() {
+ format!("Empty file: {}", file.path)
+ } else {
+ Self::expand_tabs(&content)
+ }
+ }
+ Err(_) => Self::binary_preview_notice(&file.path),
+ },
Err(err) => format!("content error for {}: {}", file.path, err),
},
_ => match self.client.diff_for(&file.path) {
Ok(diff) => if diff.trim().is_empty() { format!("No diff for {}", file.path) } else { diff },
Err(err) => format!("diff error for {}: {}", file.path, err),
@@ -131,10 +141,23 @@
} else {
self.state.diff = Some("No file selected".to_string());
}
}
}
+
+ fn expand_tabs(input: &str) -> String {
+ input.replace('\t', " ")
+ }
+
+ fn binary_preview_notice(path: &str) -> String {
+ let name = Path::new(path).file_name().and_then(|n| n.to_str()).unwrap_or(path);
+ let logo = ASCII_LOGO.trim_end();
+ format!(
+ "Preview unavailable for {}\n\n{}\n\nOpen externally or use hex view",
+ name, logo
+ )
+ }
fn current_file_path(&self) -> Option<String> {
self.state.repo.as_ref()?.files.get(self.state.repo.as_ref()?.selected_file).map(|f| f.path.clone())
}
@@ -195,12 +218,20 @@
let extras: Vec<String> = paths
.iter()
.filter_map(|path| repo.files.iter().find(|f| &f.path == path && f.status == "extra").map(|f| f.path.clone()))
.collect();
+ let binary_files: Vec<String> = paths
+ .iter()
+ .filter(|path| is_binary_path(path))
+ .cloned()
+ .collect();
let result = (|| {
+ if !binary_files.is_empty() {
+ self.client.set_binary_glob("*.png,*.jpg,*.jpeg,*.gif,*.ico")?;
+ }
if !extras.is_empty() {
self.client.add_files(&extras)?;
}
self.client.commit_paths(&paths, &message)
})();
@@ -295,10 +326,15 @@
}
}
Ok(())
}
}
+
+fn is_binary_path(path: &str) -> bool {
+ let lower = path.to_ascii_lowercase();
+ [".png", ".jpg", ".jpeg", ".gif", ".ico"].iter().any(|ext| lower.ends_with(ext))
+}
#[cfg(test)]
mod tests {
use super::*;
use crate::fossil::FileStatus;
=== fossil info ===
status: ok
stdout:
project-name: lazyfossil
repository: /home/geraldo/museum/lazyfossil.fossil
local-root: /home/geraldo/Documents/pi_lazyfossil/
config-db: /home/geraldo/.config/fossil.db
project-code: 3c93e1c05dd9744de1e9512b949b2d45d8052f60
checkout: fff8b35f249823910aab68967a8273dfa37daf69 2026-06-06 07:13:07 UTC
parent: bab3996240954064d8ad695417e2d1f6b7d4bbdb 2026-06-06 05:02:24 UTC
tags: trunk
comment: Add project logo (user: geraldo)
check-ins: 25
=== fossil status ===
status: ok
stdout:
repository: /home/geraldo/museum/lazyfossil.fossil
local-root: /home/geraldo/Documents/pi_lazyfossil/
config-db: /home/geraldo/.config/fossil.db
checkout: fff8b35f249823910aab68967a8273dfa37daf69 2026-06-06 07:13:07 UTC
parent: bab3996240954064d8ad695417e2d1f6b7d4bbdb 2026-06-06 05:02:24 UTC
tags: trunk
comment: Add project logo (user: geraldo)
EDITED src/app.rs
EDITED src/fossil.rs
EDITED src/ui.rs
stderr:
setting ignore-glob has both versioned and non-versioned values: using versioned value from file "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob" (to silence this warning, either create an empty file named "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob.no-warn" in the check-out root, or delete the non-versioned setting with "fossil unset ignore-glob")
=== fossil extras --dotfiles ===
status: ok
stdout:
.pi/plans/2026-06-03-fossil-tui-mvp.md
.pi/plans/2026-06-03-rethink-commit-selection.md
.pi/plans/2026-06-04-fossil-tui-mvp.md
README.bak
crates.io-athena-token
doc/images/lazyfossil_logo_2_high_res.png
stderr:
setting ignore-glob has both versioned and non-versioned values: using versioned value from file "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob" (to silence this warning, either create an empty file named "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob.no-warn" in the check-out root, or delete the non-versioned setting with "fossil unset ignore-glob")
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c ===
status: ok
stdout:
fff8b35f24|geraldo|2026-06-06 07:13:07|Add project logo
bab3996240|geraldo|2026-06-06 05:02:24|Add publish github release workflow
760df322ef|geraldo|2026-06-05 20:06:18|Add badge to README
1ac6b345e4|geraldo|2026-06-05 20:05:30|Fix github workflow binary path
67774b0a64|geraldo|2026-06-05 19:05:41|fix github workflow - binary_name
e507e22d1b|geraldo|2026-06-05 18:50:53|Bump version to 0.3.1 and rebuild
de8e50f7b0|geraldo|2026-06-05 18:25:33|Add github workflow
dbe9c779db|geraldo|2026-06-05 18:12:57|Update repository to github
d82a046b8e|geraldo|2026-06-05 18:08:41|Mirror fossil repository to github
14b139337f|geraldo|2026-06-05 18:06:58|Include hidden files in extra listing
3e4f100ab4|geraldo|2026-06-05 18:03:02|Add sync feature
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0bc11330ff|geraldo|2026-06-04 18:02:47|Add test cases
0b5ee26b73|geraldo|2026-06-03 20:21:03|Update README
1094a55d21|geraldo|2026-06-03 20:10:05|Implement commit selection flow and bump version to 0.1.3
daa11aa0c1|geraldo|2026-06-03 20:07:51|Add README
b66b54d85b|geraldo|2026-06-03 20:04:50|Update version in Cargo.lock
f6f5577549|geraldo|2026-06-03 19:48:00|Bump version to 0.1.2 and save current TUI progress
504d232dbd|geraldo|2026-06-03 18:40:24|Preview extra files and bump version to 0.1.1
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p src/app.rs ===
status: ok
stdout:
3e4f100ab4|geraldo|2026-06-05 18:03:02|Add sync feature
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0bc11330ff|geraldo|2026-06-04 18:02:47|Add test cases
1094a55d21|geraldo|2026-06-03 20:10:05|Implement commit selection flow and bump version to 0.1.3
f6f5577549|geraldo|2026-06-03 19:48:00|Bump version to 0.1.2 and save current TUI progress
504d232dbd|geraldo|2026-06-03 18:40:24|Preview extra files and bump version to 0.1.1
05ae23bdc5|geraldo|2026-06-02 19:57:40|Add mouse file selection and diff scrolling
e7057da18b|geraldo|2026-06-02 19:52:08|Fix selected-file diff and footer status line
49410bfd5e|geraldo|2026-06-02 19:18:34|Initial lazyfossil TUI MVP
=== fossil diff -- src/app.rs ===
status: ok
stdout:
Index: src/app.rs
==================================================================
@@ -6,10 +6,13 @@
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use std::fs;
use std::io;
+use std::path::Path;
+
+const ASCII_LOGO: &str = include_str!("../doc/images/lazyfossil_logo_01.txt");
use std::time::Duration;
pub fn run() -> Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
@@ -115,14 +118,21 @@
fn refresh_diff(&mut self) {
if let Some(repo) = &self.state.repo {
if let Some(file) = repo.files.get(repo.selected_file) {
self.state.diff_scroll = 0;
self.state.diff = Some(match file.status.as_str() {
- "extra" => match fs::read_to_string(&file.path) {
- Ok(content) => {
- if content.trim().is_empty() { format!("Empty file: {}", file.path) } else { content }
- }
+ "extra" => match fs::read(&file.path) {
+ Ok(bytes) => match String::from_utf8(bytes) {
+ Ok(content) => {
+ if content.trim().is_empty() {
+ format!("Empty file: {}", file.path)
+ } else {
+ Self::expand_tabs(&content)
+ }
+ }
+ Err(_) => Self::binary_preview_notice(&file.path),
+ },
Err(err) => format!("content error for {}: {}", file.path, err),
},
_ => match self.client.diff_for(&file.path) {
Ok(diff) => if diff.trim().is_empty() { format!("No diff for {}", file.path) } else { diff },
Err(err) => format!("diff error for {}: {}", file.path, err),
@@ -131,10 +141,23 @@
} else {
self.state.diff = Some("No file selected".to_string());
}
}
}
+
+ fn expand_tabs(input: &str) -> String {
+ input.replace('\t', " ")
+ }
+
+ fn binary_preview_notice(path: &str) -> String {
+ let name = Path::new(path).file_name().and_then(|n| n.to_str()).unwrap_or(path);
+ let logo = ASCII_LOGO.trim_end();
+ format!(
+ "Preview unavailable for {}\n\n{}\n\nOpen externally or use hex view",
+ name, logo
+ )
+ }
fn current_file_path(&self) -> Option<String> {
self.state.repo.as_ref()?.files.get(self.state.repo.as_ref()?.selected_file).map(|f| f.path.clone())
}
@@ -195,12 +218,20 @@
let extras: Vec<String> = paths
.iter()
.filter_map(|path| repo.files.iter().find(|f| &f.path == path && f.status == "extra").map(|f| f.path.clone()))
.collect();
+ let binary_files: Vec<String> = paths
+ .iter()
+ .filter(|path| is_binary_path(path))
+ .cloned()
+ .collect();
let result = (|| {
+ if !binary_files.is_empty() {
+ self.client.set_binary_glob("*.png,*.jpg,*.jpeg,*.gif,*.ico")?;
+ }
if !extras.is_empty() {
self.client.add_files(&extras)?;
}
self.client.commit_paths(&paths, &message)
})();
@@ -295,10 +326,15 @@
}
}
Ok(())
}
}
+
+fn is_binary_path(path: &str) -> bool {
+ let lower = path.to_ascii_lowercase();
+ [".png", ".jpg", ".jpeg", ".gif", ".ico"].iter().any(|ext| lower.ends_with(ext))
+}
#[cfg(test)]
mod tests {
use super::*;
use crate::fossil::FileStatus;
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p src/fossil.rs ===
status: ok
stdout:
14b139337f|geraldo|2026-06-05 18:06:58|Include hidden files in extra listing
3e4f100ab4|geraldo|2026-06-05 18:03:02|Add sync feature
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0bc11330ff|geraldo|2026-06-04 18:02:47|Add test cases
1094a55d21|geraldo|2026-06-03 20:10:05|Implement commit selection flow and bump version to 0.1.3
f6f5577549|geraldo|2026-06-03 19:48:00|Bump version to 0.1.2 and save current TUI progress
504d232dbd|geraldo|2026-06-03 18:40:24|Preview extra files and bump version to 0.1.1
e7057da18b|geraldo|2026-06-02 19:52:08|Fix selected-file diff and footer status line
49410bfd5e|geraldo|2026-06-02 19:18:34|Initial lazyfossil TUI MVP
=== fossil diff -- src/fossil.rs ===
status: ok
stdout:
Index: src/fossil.rs
==================================================================
@@ -100,10 +100,14 @@
pub fn ignore_glob(&self, pattern: &str) -> std::result::Result<String, FossilError> {
update_ignore_file(pattern).map_err(|e| FossilError::CommandFailed(e.to_string()))?;
Ok(format!("ignored {}", pattern))
}
+
+ pub fn set_binary_glob(&self, pattern: &str) -> std::result::Result<String, FossilError> {
+ self.run(&["settings", "binary-glob", pattern])
+ }
pub fn cat_file(&self, path: &str) -> std::result::Result<String, FossilError> {
self.run(&["cat", path])
}
@@ -147,16 +151,14 @@
}
args
}
fn build_commit_args<'a>(paths: &'a [String], message: &'a str) -> Vec<&'a str> {
- let mut args = vec!["commit"];
+ let mut args = vec!["commit", "-m", message];
for path in paths {
args.push(path.as_str());
}
- args.push("-m");
- args.push(message);
args
}
fn update_ignore_file(pattern: &str) -> std::io::Result<()> {
let dir = ".fossil-settings";
@@ -275,11 +277,11 @@
#[test]
fn builds_commit_arguments_for_selected_paths() {
let paths = vec!["a.txt".to_string(), "b.txt".to_string()];
let args = build_commit_args(&paths, "hello");
- assert_eq!(args, vec!["commit", "a.txt", "b.txt", "-m", "hello"]);
+ assert_eq!(args, vec!["commit", "-m", "hello", "a.txt", "b.txt"]);
}
#[test]
fn builds_add_arguments_for_selected_paths() {
let paths = vec!["extra.txt".to_string()];
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p src/app.rs ===
status: ok
stdout:
3e4f100ab4|geraldo|2026-06-05 18:03:02|Add sync feature
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0bc11330ff|geraldo|2026-06-04 18:02:47|Add test cases
1094a55d21|geraldo|2026-06-03 20:10:05|Implement commit selection flow and bump version to 0.1.3
f6f5577549|geraldo|2026-06-03 19:48:00|Bump version to 0.1.2 and save current TUI progress
504d232dbd|geraldo|2026-06-03 18:40:24|Preview extra files and bump version to 0.1.1
05ae23bdc5|geraldo|2026-06-02 19:57:40|Add mouse file selection and diff scrolling
e7057da18b|geraldo|2026-06-02 19:52:08|Fix selected-file diff and footer status line
49410bfd5e|geraldo|2026-06-02 19:18:34|Initial lazyfossil TUI MVP
=== fossil diff -- src/app.rs ===
status: ok
stdout:
Index: src/app.rs
==================================================================
@@ -6,10 +6,13 @@
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use std::fs;
use std::io;
+use std::path::Path;
+
+const ASCII_LOGO: &str = include_str!("../doc/images/lazyfossil_logo_01.txt");
use std::time::Duration;
pub fn run() -> Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
@@ -115,14 +118,21 @@
fn refresh_diff(&mut self) {
if let Some(repo) = &self.state.repo {
if let Some(file) = repo.files.get(repo.selected_file) {
self.state.diff_scroll = 0;
self.state.diff = Some(match file.status.as_str() {
- "extra" => match fs::read_to_string(&file.path) {
- Ok(content) => {
- if content.trim().is_empty() { format!("Empty file: {}", file.path) } else { content }
- }
+ "extra" => match fs::read(&file.path) {
+ Ok(bytes) => match String::from_utf8(bytes) {
+ Ok(content) => {
+ if content.trim().is_empty() {
+ format!("Empty file: {}", file.path)
+ } else {
+ Self::expand_tabs(&content)
+ }
+ }
+ Err(_) => Self::binary_preview_notice(&file.path),
+ },
Err(err) => format!("content error for {}: {}", file.path, err),
},
_ => match self.client.diff_for(&file.path) {
Ok(diff) => if diff.trim().is_empty() { format!("No diff for {}", file.path) } else { diff },
Err(err) => format!("diff error for {}: {}", file.path, err),
@@ -131,10 +141,23 @@
} else {
self.state.diff = Some("No file selected".to_string());
}
}
}
+
+ fn expand_tabs(input: &str) -> String {
+ input.replace('\t', " ")
+ }
+
+ fn binary_preview_notice(path: &str) -> String {
+ let name = Path::new(path).file_name().and_then(|n| n.to_str()).unwrap_or(path);
+ let logo = ASCII_LOGO.trim_end();
+ format!(
+ "Preview unavailable for {}\n\n{}\n\nOpen externally or use hex view",
+ name, logo
+ )
+ }
fn current_file_path(&self) -> Option<String> {
self.state.repo.as_ref()?.files.get(self.state.repo.as_ref()?.selected_file).map(|f| f.path.clone())
}
@@ -195,12 +218,20 @@
let extras: Vec<String> = paths
.iter()
.filter_map(|path| repo.files.iter().find(|f| &f.path == path && f.status == "extra").map(|f| f.path.clone()))
.collect();
+ let binary_files: Vec<String> = paths
+ .iter()
+ .filter(|path| is_binary_path(path))
+ .cloned()
+ .collect();
let result = (|| {
+ if !binary_files.is_empty() {
+ self.client.set_binary_glob("*.png,*.jpg,*.jpeg,*.gif,*.ico")?;
+ }
if !extras.is_empty() {
self.client.add_files(&extras)?;
}
self.client.commit_paths(&paths, &message)
})();
@@ -295,10 +326,15 @@
}
}
Ok(())
}
}
+
+fn is_binary_path(path: &str) -> bool {
+ let lower = path.to_ascii_lowercase();
+ [".png", ".jpg", ".jpeg", ".gif", ".ico"].iter().any(|ext| lower.ends_with(ext))
+}
#[cfg(test)]
mod tests {
use super::*;
use crate::fossil::FileStatus;
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p src/fossil.rs ===
status: ok
stdout:
14b139337f|geraldo|2026-06-05 18:06:58|Include hidden files in extra listing
3e4f100ab4|geraldo|2026-06-05 18:03:02|Add sync feature
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0bc11330ff|geraldo|2026-06-04 18:02:47|Add test cases
1094a55d21|geraldo|2026-06-03 20:10:05|Implement commit selection flow and bump version to 0.1.3
f6f5577549|geraldo|2026-06-03 19:48:00|Bump version to 0.1.2 and save current TUI progress
504d232dbd|geraldo|2026-06-03 18:40:24|Preview extra files and bump version to 0.1.1
e7057da18b|geraldo|2026-06-02 19:52:08|Fix selected-file diff and footer status line
49410bfd5e|geraldo|2026-06-02 19:18:34|Initial lazyfossil TUI MVP
=== fossil diff -- src/fossil.rs ===
status: ok
stdout:
Index: src/fossil.rs
==================================================================
@@ -100,10 +100,14 @@
pub fn ignore_glob(&self, pattern: &str) -> std::result::Result<String, FossilError> {
update_ignore_file(pattern).map_err(|e| FossilError::CommandFailed(e.to_string()))?;
Ok(format!("ignored {}", pattern))
}
+
+ pub fn set_binary_glob(&self, pattern: &str) -> std::result::Result<String, FossilError> {
+ self.run(&["settings", "binary-glob", pattern])
+ }
pub fn cat_file(&self, path: &str) -> std::result::Result<String, FossilError> {
self.run(&["cat", path])
}
@@ -147,16 +151,14 @@
}
args
}
fn build_commit_args<'a>(paths: &'a [String], message: &'a str) -> Vec<&'a str> {
- let mut args = vec!["commit"];
+ let mut args = vec!["commit", "-m", message];
for path in paths {
args.push(path.as_str());
}
- args.push("-m");
- args.push(message);
args
}
fn update_ignore_file(pattern: &str) -> std::io::Result<()> {
let dir = ".fossil-settings";
@@ -275,11 +277,11 @@
#[test]
fn builds_commit_arguments_for_selected_paths() {
let paths = vec!["a.txt".to_string(), "b.txt".to_string()];
let args = build_commit_args(&paths, "hello");
- assert_eq!(args, vec!["commit", "a.txt", "b.txt", "-m", "hello"]);
+ assert_eq!(args, vec!["commit", "-m", "hello", "a.txt", "b.txt"]);
}
#[test]
fn builds_add_arguments_for_selected_paths() {
let paths = vec!["extra.txt".to_string()];
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p src/ui.rs ===
status: ok
stdout:
e507e22d1b|geraldo|2026-06-05 18:50:53|Bump version to 0.3.1 and rebuild
3e4f100ab4|geraldo|2026-06-05 18:03:02|Add sync feature
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
1094a55d21|geraldo|2026-06-03 20:10:05|Implement commit selection flow and bump version to 0.1.3
f6f5577549|geraldo|2026-06-03 19:48:00|Bump version to 0.1.2 and save current TUI progress
504d232dbd|geraldo|2026-06-03 18:40:24|Preview extra files and bump version to 0.1.1
9847421853|geraldo|2026-06-02 20:15:33|Prepare crates.io metadata and docs
05ae23bdc5|geraldo|2026-06-02 19:57:40|Add mouse file selection and diff scrolling
e7057da18b|geraldo|2026-06-02 19:52:08|Fix selected-file diff and footer status line
49410bfd5e|geraldo|2026-06-02 19:18:34|Initial lazyfossil TUI MVP
=== fossil diff -- src/ui.rs ===
status: ok
stdout:
Index: src/ui.rs
==================================================================
@@ -116,11 +116,13 @@
}
}
fn color_diff(diff: String) -> Text<'static> {
Text::from(diff.lines().map(|line| {
- let style = if line.starts_with("+++") || line.starts_with("---") { Style::default().fg(Color::Blue) }
+ let style = if line.starts_with("Preview unavailable for ") {
+ Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
+ } else if line.starts_with("+++") || line.starts_with("---") { Style::default().fg(Color::Blue) }
else if line.starts_with("@@") { Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD) }
else if line.starts_with('+') { Style::default().fg(Color::Green) }
else if line.starts_with('-') { Style::default().fg(Color::Red) }
else { Style::default().fg(Color::Reset) };
Line::from(Span::styled(line.to_string(), style))
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p src/fossil.rs ===
status: ok
stdout:
14b139337f|geraldo|2026-06-05 18:06:58|Include hidden files in extra listing
3e4f100ab4|geraldo|2026-06-05 18:03:02|Add sync feature
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0bc11330ff|geraldo|2026-06-04 18:02:47|Add test cases
1094a55d21|geraldo|2026-06-03 20:10:05|Implement commit selection flow and bump version to 0.1.3
f6f5577549|geraldo|2026-06-03 19:48:00|Bump version to 0.1.2 and save current TUI progress
504d232dbd|geraldo|2026-06-03 18:40:24|Preview extra files and bump version to 0.1.1
e7057da18b|geraldo|2026-06-02 19:52:08|Fix selected-file diff and footer status line
49410bfd5e|geraldo|2026-06-02 19:18:34|Initial lazyfossil TUI MVP
=== fossil diff -- src/fossil.rs ===
status: ok
stdout:
Index: src/fossil.rs
==================================================================
@@ -100,10 +100,14 @@
pub fn ignore_glob(&self, pattern: &str) -> std::result::Result<String, FossilError> {
update_ignore_file(pattern).map_err(|e| FossilError::CommandFailed(e.to_string()))?;
Ok(format!("ignored {}", pattern))
}
+
+ pub fn set_binary_glob(&self, pattern: &str) -> std::result::Result<String, FossilError> {
+ self.run(&["settings", "binary-glob", pattern])
+ }
pub fn cat_file(&self, path: &str) -> std::result::Result<String, FossilError> {
self.run(&["cat", path])
}
@@ -147,16 +151,14 @@
}
args
}
fn build_commit_args<'a>(paths: &'a [String], message: &'a str) -> Vec<&'a str> {
- let mut args = vec!["commit"];
+ let mut args = vec!["commit", "-m", message];
for path in paths {
args.push(path.as_str());
}
- args.push("-m");
- args.push(message);
args
}
fn update_ignore_file(pattern: &str) -> std::io::Result<()> {
let dir = ".fossil-settings";
@@ -275,11 +277,11 @@
#[test]
fn builds_commit_arguments_for_selected_paths() {
let paths = vec!["a.txt".to_string(), "b.txt".to_string()];
let args = build_commit_args(&paths, "hello");
- assert_eq!(args, vec!["commit", "a.txt", "b.txt", "-m", "hello"]);
+ assert_eq!(args, vec!["commit", "-m", "hello", "a.txt", "b.txt"]);
}
#[test]
fn builds_add_arguments_for_selected_paths() {
let paths = vec!["extra.txt".to_string()];
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p src/app.rs ===
status: ok
stdout:
3e4f100ab4|geraldo|2026-06-05 18:03:02|Add sync feature
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0bc11330ff|geraldo|2026-06-04 18:02:47|Add test cases
1094a55d21|geraldo|2026-06-03 20:10:05|Implement commit selection flow and bump version to 0.1.3
f6f5577549|geraldo|2026-06-03 19:48:00|Bump version to 0.1.2 and save current TUI progress
504d232dbd|geraldo|2026-06-03 18:40:24|Preview extra files and bump version to 0.1.1
05ae23bdc5|geraldo|2026-06-02 19:57:40|Add mouse file selection and diff scrolling
e7057da18b|geraldo|2026-06-02 19:52:08|Fix selected-file diff and footer status line
49410bfd5e|geraldo|2026-06-02 19:18:34|Initial lazyfossil TUI MVP
=== fossil diff -- src/app.rs ===
status: ok
stdout:
Index: src/app.rs
==================================================================
@@ -6,10 +6,13 @@
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use std::fs;
use std::io;
+use std::path::Path;
+
+const ASCII_LOGO: &str = include_str!("../doc/images/lazyfossil_logo_01.txt");
use std::time::Duration;
pub fn run() -> Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
@@ -115,14 +118,21 @@
fn refresh_diff(&mut self) {
if let Some(repo) = &self.state.repo {
if let Some(file) = repo.files.get(repo.selected_file) {
self.state.diff_scroll = 0;
self.state.diff = Some(match file.status.as_str() {
- "extra" => match fs::read_to_string(&file.path) {
- Ok(content) => {
- if content.trim().is_empty() { format!("Empty file: {}", file.path) } else { content }
- }
+ "extra" => match fs::read(&file.path) {
+ Ok(bytes) => match String::from_utf8(bytes) {
+ Ok(content) => {
+ if content.trim().is_empty() {
+ format!("Empty file: {}", file.path)
+ } else {
+ Self::expand_tabs(&content)
+ }
+ }
+ Err(_) => Self::binary_preview_notice(&file.path),
+ },
Err(err) => format!("content error for {}: {}", file.path, err),
},
_ => match self.client.diff_for(&file.path) {
Ok(diff) => if diff.trim().is_empty() { format!("No diff for {}", file.path) } else { diff },
Err(err) => format!("diff error for {}: {}", file.path, err),
@@ -131,10 +141,23 @@
} else {
self.state.diff = Some("No file selected".to_string());
}
}
}
+
+ fn expand_tabs(input: &str) -> String {
+ input.replace('\t', " ")
+ }
+
+ fn binary_preview_notice(path: &str) -> String {
+ let name = Path::new(path).file_name().and_then(|n| n.to_str()).unwrap_or(path);
+ let logo = ASCII_LOGO.trim_end();
+ format!(
+ "Preview unavailable for {}\n\n{}\n\nOpen externally or use hex view",
+ name, logo
+ )
+ }
fn current_file_path(&self) -> Option<String> {
self.state.repo.as_ref()?.files.get(self.state.repo.as_ref()?.selected_file).map(|f| f.path.clone())
}
@@ -195,12 +218,20 @@
let extras: Vec<String> = paths
.iter()
.filter_map(|path| repo.files.iter().find(|f| &f.path == path && f.status == "extra").map(|f| f.path.clone()))
.collect();
+ let binary_files: Vec<String> = paths
+ .iter()
+ .filter(|path| is_binary_path(path))
+ .cloned()
+ .collect();
let result = (|| {
+ if !binary_files.is_empty() {
+ self.client.set_binary_glob("*.png,*.jpg,*.jpeg,*.gif,*.ico")?;
+ }
if !extras.is_empty() {
self.client.add_files(&extras)?;
}
self.client.commit_paths(&paths, &message)
})();
@@ -295,10 +326,15 @@
}
}
Ok(())
}
}
+
+fn is_binary_path(path: &str) -> bool {
+ let lower = path.to_ascii_lowercase();
+ [".png", ".jpg", ".jpeg", ".gif", ".ico"].iter().any(|ext| lower.ends_with(ext))
+}
#[cfg(test)]
mod tests {
use super::*;
use crate::fossil::FileStatus;
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p doc/images/lazyfossil_logo_2_high_res.png ===
status: ok
stdout:
=== fossil info ===
status: ok
stdout:
project-name: lazyfossil
repository: /home/geraldo/museum/lazyfossil.fossil
local-root: /home/geraldo/Documents/pi_lazyfossil/
config-db: /home/geraldo/.config/fossil.db
project-code: 3c93e1c05dd9744de1e9512b949b2d45d8052f60
checkout: fff8b35f249823910aab68967a8273dfa37daf69 2026-06-06 07:13:07 UTC
parent: bab3996240954064d8ad695417e2d1f6b7d4bbdb 2026-06-06 05:02:24 UTC
tags: trunk
comment: Add project logo (user: geraldo)
check-ins: 25
=== fossil status ===
status: ok
stdout:
repository: /home/geraldo/museum/lazyfossil.fossil
local-root: /home/geraldo/Documents/pi_lazyfossil/
config-db: /home/geraldo/.config/fossil.db
checkout: fff8b35f249823910aab68967a8273dfa37daf69 2026-06-06 07:13:07 UTC
parent: bab3996240954064d8ad695417e2d1f6b7d4bbdb 2026-06-06 05:02:24 UTC
tags: trunk
comment: Add project logo (user: geraldo)
EDITED README.md
EDITED src/app.rs
EDITED src/fossil.rs
EDITED src/ui.rs
stderr:
setting ignore-glob has both versioned and non-versioned values: using versioned value from file "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob" (to silence this warning, either create an empty file named "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob.no-warn" in the check-out root, or delete the non-versioned setting with "fossil unset ignore-glob")
=== fossil extras --dotfiles ===
status: ok
stdout:
.pi/plans/2026-06-03-fossil-tui-mvp.md
.pi/plans/2026-06-03-rethink-commit-selection.md
.pi/plans/2026-06-04-fossil-tui-mvp.md
README.bak
crates.io-athena-token
doc/images/lazyfossil_logo_2_high_res.png
stderr:
setting ignore-glob has both versioned and non-versioned values: using versioned value from file "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob" (to silence this warning, either create an empty file named "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob.no-warn" in the check-out root, or delete the non-versioned setting with "fossil unset ignore-glob")
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c ===
status: ok
stdout:
fff8b35f24|geraldo|2026-06-06 07:13:07|Add project logo
bab3996240|geraldo|2026-06-06 05:02:24|Add publish github release workflow
760df322ef|geraldo|2026-06-05 20:06:18|Add badge to README
1ac6b345e4|geraldo|2026-06-05 20:05:30|Fix github workflow binary path
67774b0a64|geraldo|2026-06-05 19:05:41|fix github workflow - binary_name
e507e22d1b|geraldo|2026-06-05 18:50:53|Bump version to 0.3.1 and rebuild
de8e50f7b0|geraldo|2026-06-05 18:25:33|Add github workflow
dbe9c779db|geraldo|2026-06-05 18:12:57|Update repository to github
d82a046b8e|geraldo|2026-06-05 18:08:41|Mirror fossil repository to github
14b139337f|geraldo|2026-06-05 18:06:58|Include hidden files in extra listing
3e4f100ab4|geraldo|2026-06-05 18:03:02|Add sync feature
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0bc11330ff|geraldo|2026-06-04 18:02:47|Add test cases
0b5ee26b73|geraldo|2026-06-03 20:21:03|Update README
1094a55d21|geraldo|2026-06-03 20:10:05|Implement commit selection flow and bump version to 0.1.3
daa11aa0c1|geraldo|2026-06-03 20:07:51|Add README
b66b54d85b|geraldo|2026-06-03 20:04:50|Update version in Cargo.lock
f6f5577549|geraldo|2026-06-03 19:48:00|Bump version to 0.1.2 and save current TUI progress
504d232dbd|geraldo|2026-06-03 18:40:24|Preview extra files and bump version to 0.1.1
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p README.md ===
status: ok
stdout:
fff8b35f24|geraldo|2026-06-06 07:13:07|Add project logo
760df322ef|geraldo|2026-06-05 20:06:18|Add badge to README
e507e22d1b|geraldo|2026-06-05 18:50:53|Bump version to 0.3.1 and rebuild
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0b5ee26b73|geraldo|2026-06-03 20:21:03|Update README
daa11aa0c1|geraldo|2026-06-03 20:07:51|Add README
=== fossil diff -- README.md ===
status: ok
stdout:
Index: README.md
==================================================================
@@ -62,5 +62,16 @@
## Versioning
This project follows semantic versioning: `MAJOR.MINOR.PATCH`.
Current version: `0.3.1`.
+## Credits
+
+### [pi.dev](https://pi.dev)
+Pi provides the agent harness used to shape and iterate on this project. Its tooling made it possible to refine the TUI, validate changes, and keep the implementation moving quickly.
+
+### [crates.io/crates/lazyfossil](https://crates.io/crates/lazyfossil)
+The crate listing is the distribution point for the Rust application, making the project available to the wider Rust ecosystem and simplifying installation and release management.
+
+### [emojicombos.com/lazyfossil](https://emojicombos.com/lazyfossil)
+This source provided the project logo artwork used in the README and assets, helping give lazyfossil a recognizable visual identity.
+
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p src/app.rs ===
status: ok
stdout:
3e4f100ab4|geraldo|2026-06-05 18:03:02|Add sync feature
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0bc11330ff|geraldo|2026-06-04 18:02:47|Add test cases
1094a55d21|geraldo|2026-06-03 20:10:05|Implement commit selection flow and bump version to 0.1.3
f6f5577549|geraldo|2026-06-03 19:48:00|Bump version to 0.1.2 and save current TUI progress
504d232dbd|geraldo|2026-06-03 18:40:24|Preview extra files and bump version to 0.1.1
05ae23bdc5|geraldo|2026-06-02 19:57:40|Add mouse file selection and diff scrolling
e7057da18b|geraldo|2026-06-02 19:52:08|Fix selected-file diff and footer status line
49410bfd5e|geraldo|2026-06-02 19:18:34|Initial lazyfossil TUI MVP
=== fossil diff -- src/app.rs ===
status: ok
stdout:
Index: src/app.rs
==================================================================
@@ -6,10 +6,13 @@
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use std::fs;
use std::io;
+use std::path::Path;
+
+const ASCII_LOGO: &str = include_str!("../doc/images/lazyfossil_logo_01.txt");
use std::time::Duration;
pub fn run() -> Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
@@ -115,14 +118,21 @@
fn refresh_diff(&mut self) {
if let Some(repo) = &self.state.repo {
if let Some(file) = repo.files.get(repo.selected_file) {
self.state.diff_scroll = 0;
self.state.diff = Some(match file.status.as_str() {
- "extra" => match fs::read_to_string(&file.path) {
- Ok(content) => {
- if content.trim().is_empty() { format!("Empty file: {}", file.path) } else { content }
- }
+ "extra" => match fs::read(&file.path) {
+ Ok(bytes) => match String::from_utf8(bytes) {
+ Ok(content) => {
+ if content.trim().is_empty() {
+ format!("Empty file: {}", file.path)
+ } else {
+ Self::expand_tabs(&content)
+ }
+ }
+ Err(_) => Self::binary_preview_notice(&file.path),
+ },
Err(err) => format!("content error for {}: {}", file.path, err),
},
_ => match self.client.diff_for(&file.path) {
Ok(diff) => if diff.trim().is_empty() { format!("No diff for {}", file.path) } else { diff },
Err(err) => format!("diff error for {}: {}", file.path, err),
@@ -131,10 +141,23 @@
} else {
self.state.diff = Some("No file selected".to_string());
}
}
}
+
+ fn expand_tabs(input: &str) -> String {
+ input.replace('\t', " ")
+ }
+
+ fn binary_preview_notice(path: &str) -> String {
+ let name = Path::new(path).file_name().and_then(|n| n.to_str()).unwrap_or(path);
+ let logo = ASCII_LOGO.trim_end();
+ format!(
+ "Preview unavailable for {}\n\n{}\n\nOpen externally or use hex view",
+ name, logo
+ )
+ }
fn current_file_path(&self) -> Option<String> {
self.state.repo.as_ref()?.files.get(self.state.repo.as_ref()?.selected_file).map(|f| f.path.clone())
}
@@ -195,12 +218,20 @@
let extras: Vec<String> = paths
.iter()
.filter_map(|path| repo.files.iter().find(|f| &f.path == path && f.status == "extra").map(|f| f.path.clone()))
.collect();
+ let binary_files: Vec<String> = paths
+ .iter()
+ .filter(|path| is_binary_path(path))
+ .cloned()
+ .collect();
let result = (|| {
+ if !binary_files.is_empty() {
+ self.client.set_binary_glob("*.png,*.jpg,*.jpeg,*.gif,*.ico")?;
+ }
if !extras.is_empty() {
self.client.add_files(&extras)?;
}
self.client.commit_paths(&paths, &message)
})();
@@ -295,10 +326,15 @@
}
}
Ok(())
}
}
+
+fn is_binary_path(path: &str) -> bool {
+ let lower = path.to_ascii_lowercase();
+ [".png", ".jpg", ".jpeg", ".gif", ".ico"].iter().any(|ext| lower.ends_with(ext))
+}
#[cfg(test)]
mod tests {
use super::*;
use crate::fossil::FileStatus;
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p src/fossil.rs ===
status: ok
stdout:
14b139337f|geraldo|2026-06-05 18:06:58|Include hidden files in extra listing
3e4f100ab4|geraldo|2026-06-05 18:03:02|Add sync feature
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0bc11330ff|geraldo|2026-06-04 18:02:47|Add test cases
1094a55d21|geraldo|2026-06-03 20:10:05|Implement commit selection flow and bump version to 0.1.3
f6f5577549|geraldo|2026-06-03 19:48:00|Bump version to 0.1.2 and save current TUI progress
504d232dbd|geraldo|2026-06-03 18:40:24|Preview extra files and bump version to 0.1.1
e7057da18b|geraldo|2026-06-02 19:52:08|Fix selected-file diff and footer status line
49410bfd5e|geraldo|2026-06-02 19:18:34|Initial lazyfossil TUI MVP
=== fossil diff -- src/fossil.rs ===
status: ok
stdout:
Index: src/fossil.rs
==================================================================
@@ -100,10 +100,14 @@
pub fn ignore_glob(&self, pattern: &str) -> std::result::Result<String, FossilError> {
update_ignore_file(pattern).map_err(|e| FossilError::CommandFailed(e.to_string()))?;
Ok(format!("ignored {}", pattern))
}
+
+ pub fn set_binary_glob(&self, pattern: &str) -> std::result::Result<String, FossilError> {
+ self.run(&["settings", "binary-glob", pattern])
+ }
pub fn cat_file(&self, path: &str) -> std::result::Result<String, FossilError> {
self.run(&["cat", path])
}
@@ -147,16 +151,14 @@
}
args
}
fn build_commit_args<'a>(paths: &'a [String], message: &'a str) -> Vec<&'a str> {
- let mut args = vec!["commit"];
+ let mut args = vec!["commit", "-m", message];
for path in paths {
args.push(path.as_str());
}
- args.push("-m");
- args.push(message);
args
}
fn update_ignore_file(pattern: &str) -> std::io::Result<()> {
let dir = ".fossil-settings";
@@ -275,11 +277,11 @@
#[test]
fn builds_commit_arguments_for_selected_paths() {
let paths = vec!["a.txt".to_string(), "b.txt".to_string()];
let args = build_commit_args(&paths, "hello");
- assert_eq!(args, vec!["commit", "a.txt", "b.txt", "-m", "hello"]);
+ assert_eq!(args, vec!["commit", "-m", "hello", "a.txt", "b.txt"]);
}
#[test]
fn builds_add_arguments_for_selected_paths() {
let paths = vec!["extra.txt".to_string()];
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p src/ui.rs ===
status: ok
stdout:
e507e22d1b|geraldo|2026-06-05 18:50:53|Bump version to 0.3.1 and rebuild
3e4f100ab4|geraldo|2026-06-05 18:03:02|Add sync feature
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
1094a55d21|geraldo|2026-06-03 20:10:05|Implement commit selection flow and bump version to 0.1.3
f6f5577549|geraldo|2026-06-03 19:48:00|Bump version to 0.1.2 and save current TUI progress
504d232dbd|geraldo|2026-06-03 18:40:24|Preview extra files and bump version to 0.1.1
9847421853|geraldo|2026-06-02 20:15:33|Prepare crates.io metadata and docs
05ae23bdc5|geraldo|2026-06-02 19:57:40|Add mouse file selection and diff scrolling
e7057da18b|geraldo|2026-06-02 19:52:08|Fix selected-file diff and footer status line
49410bfd5e|geraldo|2026-06-02 19:18:34|Initial lazyfossil TUI MVP
=== fossil diff -- src/ui.rs ===
status: ok
stdout:
Index: src/ui.rs
==================================================================
@@ -116,11 +116,13 @@
}
}
fn color_diff(diff: String) -> Text<'static> {
Text::from(diff.lines().map(|line| {
- let style = if line.starts_with("+++") || line.starts_with("---") { Style::default().fg(Color::Blue) }
+ let style = if line.starts_with("Preview unavailable for ") {
+ Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
+ } else if line.starts_with("+++") || line.starts_with("---") { Style::default().fg(Color::Blue) }
else if line.starts_with("@@") { Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD) }
else if line.starts_with('+') { Style::default().fg(Color::Green) }
else if line.starts_with('-') { Style::default().fg(Color::Red) }
else { Style::default().fg(Color::Reset) };
Line::from(Span::styled(line.to_string(), style))
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-03-fossil-tui-mvp.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-03-rethink-commit-selection.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-04-fossil-tui-mvp.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p README.bak ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p crates.io-athena-token ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p README.bak ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-04-fossil-tui-mvp.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-03-rethink-commit-selection.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-03-fossil-tui-mvp.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p src/ui.rs ===
status: ok
stdout:
e507e22d1b|geraldo|2026-06-05 18:50:53|Bump version to 0.3.1 and rebuild
3e4f100ab4|geraldo|2026-06-05 18:03:02|Add sync feature
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
1094a55d21|geraldo|2026-06-03 20:10:05|Implement commit selection flow and bump version to 0.1.3
f6f5577549|geraldo|2026-06-03 19:48:00|Bump version to 0.1.2 and save current TUI progress
504d232dbd|geraldo|2026-06-03 18:40:24|Preview extra files and bump version to 0.1.1
9847421853|geraldo|2026-06-02 20:15:33|Prepare crates.io metadata and docs
05ae23bdc5|geraldo|2026-06-02 19:57:40|Add mouse file selection and diff scrolling
e7057da18b|geraldo|2026-06-02 19:52:08|Fix selected-file diff and footer status line
49410bfd5e|geraldo|2026-06-02 19:18:34|Initial lazyfossil TUI MVP
=== fossil diff -- src/ui.rs ===
status: ok
stdout:
Index: src/ui.rs
==================================================================
@@ -116,11 +116,13 @@
}
}
fn color_diff(diff: String) -> Text<'static> {
Text::from(diff.lines().map(|line| {
- let style = if line.starts_with("+++") || line.starts_with("---") { Style::default().fg(Color::Blue) }
+ let style = if line.starts_with("Preview unavailable for ") {
+ Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
+ } else if line.starts_with("+++") || line.starts_with("---") { Style::default().fg(Color::Blue) }
else if line.starts_with("@@") { Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD) }
else if line.starts_with('+') { Style::default().fg(Color::Green) }
else if line.starts_with('-') { Style::default().fg(Color::Red) }
else { Style::default().fg(Color::Reset) };
Line::from(Span::styled(line.to_string(), style))
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p src/fossil.rs ===
status: ok
stdout:
14b139337f|geraldo|2026-06-05 18:06:58|Include hidden files in extra listing
3e4f100ab4|geraldo|2026-06-05 18:03:02|Add sync feature
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0bc11330ff|geraldo|2026-06-04 18:02:47|Add test cases
1094a55d21|geraldo|2026-06-03 20:10:05|Implement commit selection flow and bump version to 0.1.3
f6f5577549|geraldo|2026-06-03 19:48:00|Bump version to 0.1.2 and save current TUI progress
504d232dbd|geraldo|2026-06-03 18:40:24|Preview extra files and bump version to 0.1.1
e7057da18b|geraldo|2026-06-02 19:52:08|Fix selected-file diff and footer status line
49410bfd5e|geraldo|2026-06-02 19:18:34|Initial lazyfossil TUI MVP
=== fossil diff -- src/fossil.rs ===
status: ok
stdout:
Index: src/fossil.rs
==================================================================
@@ -100,10 +100,14 @@
pub fn ignore_glob(&self, pattern: &str) -> std::result::Result<String, FossilError> {
update_ignore_file(pattern).map_err(|e| FossilError::CommandFailed(e.to_string()))?;
Ok(format!("ignored {}", pattern))
}
+
+ pub fn set_binary_glob(&self, pattern: &str) -> std::result::Result<String, FossilError> {
+ self.run(&["settings", "binary-glob", pattern])
+ }
pub fn cat_file(&self, path: &str) -> std::result::Result<String, FossilError> {
self.run(&["cat", path])
}
@@ -147,16 +151,14 @@
}
args
}
fn build_commit_args<'a>(paths: &'a [String], message: &'a str) -> Vec<&'a str> {
- let mut args = vec!["commit"];
+ let mut args = vec!["commit", "-m", message];
for path in paths {
args.push(path.as_str());
}
- args.push("-m");
- args.push(message);
args
}
fn update_ignore_file(pattern: &str) -> std::io::Result<()> {
let dir = ".fossil-settings";
@@ -275,11 +277,11 @@
#[test]
fn builds_commit_arguments_for_selected_paths() {
let paths = vec!["a.txt".to_string(), "b.txt".to_string()];
let args = build_commit_args(&paths, "hello");
- assert_eq!(args, vec!["commit", "a.txt", "b.txt", "-m", "hello"]);
+ assert_eq!(args, vec!["commit", "-m", "hello", "a.txt", "b.txt"]);
}
#[test]
fn builds_add_arguments_for_selected_paths() {
let paths = vec!["extra.txt".to_string()];
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p src/app.rs ===
status: ok
stdout:
3e4f100ab4|geraldo|2026-06-05 18:03:02|Add sync feature
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0bc11330ff|geraldo|2026-06-04 18:02:47|Add test cases
1094a55d21|geraldo|2026-06-03 20:10:05|Implement commit selection flow and bump version to 0.1.3
f6f5577549|geraldo|2026-06-03 19:48:00|Bump version to 0.1.2 and save current TUI progress
504d232dbd|geraldo|2026-06-03 18:40:24|Preview extra files and bump version to 0.1.1
05ae23bdc5|geraldo|2026-06-02 19:57:40|Add mouse file selection and diff scrolling
e7057da18b|geraldo|2026-06-02 19:52:08|Fix selected-file diff and footer status line
49410bfd5e|geraldo|2026-06-02 19:18:34|Initial lazyfossil TUI MVP
=== fossil diff -- src/app.rs ===
status: ok
stdout:
Index: src/app.rs
==================================================================
@@ -6,10 +6,13 @@
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use std::fs;
use std::io;
+use std::path::Path;
+
+const ASCII_LOGO: &str = include_str!("../doc/images/lazyfossil_logo_01.txt");
use std::time::Duration;
pub fn run() -> Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
@@ -115,14 +118,21 @@
fn refresh_diff(&mut self) {
if let Some(repo) = &self.state.repo {
if let Some(file) = repo.files.get(repo.selected_file) {
self.state.diff_scroll = 0;
self.state.diff = Some(match file.status.as_str() {
- "extra" => match fs::read_to_string(&file.path) {
- Ok(content) => {
- if content.trim().is_empty() { format!("Empty file: {}", file.path) } else { content }
- }
+ "extra" => match fs::read(&file.path) {
+ Ok(bytes) => match String::from_utf8(bytes) {
+ Ok(content) => {
+ if content.trim().is_empty() {
+ format!("Empty file: {}", file.path)
+ } else {
+ Self::expand_tabs(&content)
+ }
+ }
+ Err(_) => Self::binary_preview_notice(&file.path),
+ },
Err(err) => format!("content error for {}: {}", file.path, err),
},
_ => match self.client.diff_for(&file.path) {
Ok(diff) => if diff.trim().is_empty() { format!("No diff for {}", file.path) } else { diff },
Err(err) => format!("diff error for {}: {}", file.path, err),
@@ -131,10 +141,23 @@
} else {
self.state.diff = Some("No file selected".to_string());
}
}
}
+
+ fn expand_tabs(input: &str) -> String {
+ input.replace('\t', " ")
+ }
+
+ fn binary_preview_notice(path: &str) -> String {
+ let name = Path::new(path).file_name().and_then(|n| n.to_str()).unwrap_or(path);
+ let logo = ASCII_LOGO.trim_end();
+ format!(
+ "Preview unavailable for {}\n\n{}\n\nOpen externally or use hex view",
+ name, logo
+ )
+ }
fn current_file_path(&self) -> Option<String> {
self.state.repo.as_ref()?.files.get(self.state.repo.as_ref()?.selected_file).map(|f| f.path.clone())
}
@@ -195,12 +218,20 @@
let extras: Vec<String> = paths
.iter()
.filter_map(|path| repo.files.iter().find(|f| &f.path == path && f.status == "extra").map(|f| f.path.clone()))
.collect();
+ let binary_files: Vec<String> = paths
+ .iter()
+ .filter(|path| is_binary_path(path))
+ .cloned()
+ .collect();
let result = (|| {
+ if !binary_files.is_empty() {
+ self.client.set_binary_glob("*.png,*.jpg,*.jpeg,*.gif,*.ico")?;
+ }
if !extras.is_empty() {
self.client.add_files(&extras)?;
}
self.client.commit_paths(&paths, &message)
})();
@@ -295,10 +326,15 @@
}
}
Ok(())
}
}
+
+fn is_binary_path(path: &str) -> bool {
+ let lower = path.to_ascii_lowercase();
+ [".png", ".jpg", ".jpeg", ".gif", ".ico"].iter().any(|ext| lower.ends_with(ext))
+}
#[cfg(test)]
mod tests {
use super::*;
use crate::fossil::FileStatus;
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p README.md ===
status: ok
stdout:
fff8b35f24|geraldo|2026-06-06 07:13:07|Add project logo
760df322ef|geraldo|2026-06-05 20:06:18|Add badge to README
e507e22d1b|geraldo|2026-06-05 18:50:53|Bump version to 0.3.1 and rebuild
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0b5ee26b73|geraldo|2026-06-03 20:21:03|Update README
daa11aa0c1|geraldo|2026-06-03 20:07:51|Add README
=== fossil diff -- README.md ===
status: ok
stdout:
Index: README.md
==================================================================
@@ -62,5 +62,16 @@
## Versioning
This project follows semantic versioning: `MAJOR.MINOR.PATCH`.
Current version: `0.3.1`.
+## Credits
+
+### [pi.dev](https://pi.dev)
+Pi provides the agent harness used to shape and iterate on this project. Its tooling made it possible to refine the TUI, validate changes, and keep the implementation moving quickly.
+
+### [crates.io/crates/lazyfossil](https://crates.io/crates/lazyfossil)
+The crate listing is the distribution point for the Rust application, making the project available to the wider Rust ecosystem and simplifying installation and release management.
+
+### [emojicombos.com/lazyfossil](https://emojicombos.com/lazyfossil)
+This source provided the project logo artwork used in the README and assets, helping give lazyfossil a recognizable visual identity.
+
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p src/app.rs ===
status: ok
stdout:
3e4f100ab4|geraldo|2026-06-05 18:03:02|Add sync feature
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0bc11330ff|geraldo|2026-06-04 18:02:47|Add test cases
1094a55d21|geraldo|2026-06-03 20:10:05|Implement commit selection flow and bump version to 0.1.3
f6f5577549|geraldo|2026-06-03 19:48:00|Bump version to 0.1.2 and save current TUI progress
504d232dbd|geraldo|2026-06-03 18:40:24|Preview extra files and bump version to 0.1.1
05ae23bdc5|geraldo|2026-06-02 19:57:40|Add mouse file selection and diff scrolling
e7057da18b|geraldo|2026-06-02 19:52:08|Fix selected-file diff and footer status line
49410bfd5e|geraldo|2026-06-02 19:18:34|Initial lazyfossil TUI MVP
=== fossil diff -- src/app.rs ===
status: ok
stdout:
Index: src/app.rs
==================================================================
@@ -6,10 +6,13 @@
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use std::fs;
use std::io;
+use std::path::Path;
+
+const ASCII_LOGO: &str = include_str!("../doc/images/lazyfossil_logo_01.txt");
use std::time::Duration;
pub fn run() -> Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
@@ -115,14 +118,21 @@
fn refresh_diff(&mut self) {
if let Some(repo) = &self.state.repo {
if let Some(file) = repo.files.get(repo.selected_file) {
self.state.diff_scroll = 0;
self.state.diff = Some(match file.status.as_str() {
- "extra" => match fs::read_to_string(&file.path) {
- Ok(content) => {
- if content.trim().is_empty() { format!("Empty file: {}", file.path) } else { content }
- }
+ "extra" => match fs::read(&file.path) {
+ Ok(bytes) => match String::from_utf8(bytes) {
+ Ok(content) => {
+ if content.trim().is_empty() {
+ format!("Empty file: {}", file.path)
+ } else {
+ Self::expand_tabs(&content)
+ }
+ }
+ Err(_) => Self::binary_preview_notice(&file.path),
+ },
Err(err) => format!("content error for {}: {}", file.path, err),
},
_ => match self.client.diff_for(&file.path) {
Ok(diff) => if diff.trim().is_empty() { format!("No diff for {}", file.path) } else { diff },
Err(err) => format!("diff error for {}: {}", file.path, err),
@@ -131,10 +141,23 @@
} else {
self.state.diff = Some("No file selected".to_string());
}
}
}
+
+ fn expand_tabs(input: &str) -> String {
+ input.replace('\t', " ")
+ }
+
+ fn binary_preview_notice(path: &str) -> String {
+ let name = Path::new(path).file_name().and_then(|n| n.to_str()).unwrap_or(path);
+ let logo = ASCII_LOGO.trim_end();
+ format!(
+ "Preview unavailable for {}\n\n{}\n\nOpen externally or use hex view",
+ name, logo
+ )
+ }
fn current_file_path(&self) -> Option<String> {
self.state.repo.as_ref()?.files.get(self.state.repo.as_ref()?.selected_file).map(|f| f.path.clone())
}
@@ -195,12 +218,20 @@
let extras: Vec<String> = paths
.iter()
.filter_map(|path| repo.files.iter().find(|f| &f.path == path && f.status == "extra").map(|f| f.path.clone()))
.collect();
+ let binary_files: Vec<String> = paths
+ .iter()
+ .filter(|path| is_binary_path(path))
+ .cloned()
+ .collect();
let result = (|| {
+ if !binary_files.is_empty() {
+ self.client.set_binary_glob("*.png,*.jpg,*.jpeg,*.gif,*.ico")?;
+ }
if !extras.is_empty() {
self.client.add_files(&extras)?;
}
self.client.commit_paths(&paths, &message)
})();
@@ -295,10 +326,15 @@
}
}
Ok(())
}
}
+
+fn is_binary_path(path: &str) -> bool {
+ let lower = path.to_ascii_lowercase();
+ [".png", ".jpg", ".jpeg", ".gif", ".ico"].iter().any(|ext| lower.ends_with(ext))
+}
#[cfg(test)]
mod tests {
use super::*;
use crate::fossil::FileStatus;
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p doc/images/lazyfossil_logo_2_high_res.png ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p README.md ===
status: ok
stdout:
fff8b35f24|geraldo|2026-06-06 07:13:07|Add project logo
760df322ef|geraldo|2026-06-05 20:06:18|Add badge to README
e507e22d1b|geraldo|2026-06-05 18:50:53|Bump version to 0.3.1 and rebuild
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0b5ee26b73|geraldo|2026-06-03 20:21:03|Update README
daa11aa0c1|geraldo|2026-06-03 20:07:51|Add README
=== fossil diff -- README.md ===
status: ok
stdout:
Index: README.md
==================================================================
@@ -62,5 +62,16 @@
## Versioning
This project follows semantic versioning: `MAJOR.MINOR.PATCH`.
Current version: `0.3.1`.
+## Credits
+
+### [pi.dev](https://pi.dev)
+Pi provides the agent harness used to shape and iterate on this project. Its tooling made it possible to refine the TUI, validate changes, and keep the implementation moving quickly.
+
+### [crates.io/crates/lazyfossil](https://crates.io/crates/lazyfossil)
+The crate listing is the distribution point for the Rust application, making the project available to the wider Rust ecosystem and simplifying installation and release management.
+
+### [emojicombos.com/lazyfossil](https://emojicombos.com/lazyfossil)
+This source provided the project logo artwork used in the README and assets, helping give lazyfossil a recognizable visual identity.
+
=== fossil info ===
status: ok
stdout:
project-name: lazyfossil
repository: /home/geraldo/museum/lazyfossil.fossil
local-root: /home/geraldo/Documents/pi_lazyfossil/
config-db: /home/geraldo/.config/fossil.db
project-code: 3c93e1c05dd9744de1e9512b949b2d45d8052f60
checkout: 0a0984b16d5ca1dfa525c8cee32a561cbb0715a4 2026-06-06 07:26:16 UTC
parent: fff8b35f249823910aab68967a8273dfa37daf69 2026-06-06 07:13:07 UTC
tags: trunk, v0.3.2
comment: Bump version to 0.3.2 and rebuild (user: geraldo)
check-ins: 26
=== fossil status ===
status: ok
stdout:
repository: /home/geraldo/museum/lazyfossil.fossil
local-root: /home/geraldo/Documents/pi_lazyfossil/
config-db: /home/geraldo/.config/fossil.db
checkout: 0a0984b16d5ca1dfa525c8cee32a561cbb0715a4 2026-06-06 07:26:16 UTC
parent: fff8b35f249823910aab68967a8273dfa37daf69 2026-06-06 07:13:07 UTC
tags: trunk, v0.3.2
comment: Bump version to 0.3.2 and rebuild (user: geraldo)
stderr:
setting ignore-glob has both versioned and non-versioned values: using versioned value from file "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob" (to silence this warning, either create an empty file named "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob.no-warn" in the check-out root, or delete the non-versioned setting with "fossil unset ignore-glob")
=== fossil extras --dotfiles ===
status: ok
stdout:
.pi/plans/2026-06-03-fossil-tui-mvp.md
.pi/plans/2026-06-03-rethink-commit-selection.md
.pi/plans/2026-06-04-fossil-tui-mvp.md
README.bak
crates.io-athena-token
doc/images/lazyfossil_logo_2_high_res.png
stderr:
setting ignore-glob has both versioned and non-versioned values: using versioned value from file "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob" (to silence this warning, either create an empty file named "/home/geraldo/Documents/pi_lazyfossil/.fossil-settings/ignore-glob.no-warn" in the check-out root, or delete the non-versioned setting with "fossil unset ignore-glob")
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c ===
status: ok
stdout:
0a0984b16d|geraldo|2026-06-06 07:26:16|Bump version to 0.3.2 and rebuild
fff8b35f24|geraldo|2026-06-06 07:13:07|Add project logo
bab3996240|geraldo|2026-06-06 05:02:24|Add publish github release workflow
760df322ef|geraldo|2026-06-05 20:06:18|Add badge to README
1ac6b345e4|geraldo|2026-06-05 20:05:30|Fix github workflow binary path
67774b0a64|geraldo|2026-06-05 19:05:41|fix github workflow - binary_name
e507e22d1b|geraldo|2026-06-05 18:50:53|Bump version to 0.3.1 and rebuild
de8e50f7b0|geraldo|2026-06-05 18:25:33|Add github workflow
dbe9c779db|geraldo|2026-06-05 18:12:57|Update repository to github
d82a046b8e|geraldo|2026-06-05 18:08:41|Mirror fossil repository to github
14b139337f|geraldo|2026-06-05 18:06:58|Include hidden files in extra listing
3e4f100ab4|geraldo|2026-06-05 18:03:02|Add sync feature
97a7c7a052|geraldo|2026-06-04 18:32:04|Bump version to 0.2.1 and rebuild project
1e675183b9|geraldo|2026-06-04 18:21:10|Bump version to 0.2.0 and update README for semantic versioning
0bc11330ff|geraldo|2026-06-04 18:02:47|Add test cases
0b5ee26b73|geraldo|2026-06-03 20:21:03|Update README
1094a55d21|geraldo|2026-06-03 20:10:05|Implement commit selection flow and bump version to 0.1.3
daa11aa0c1|geraldo|2026-06-03 20:07:51|Add README
b66b54d85b|geraldo|2026-06-03 20:04:50|Update version in Cargo.lock
f6f5577549|geraldo|2026-06-03 19:48:00|Bump version to 0.1.2 and save current TUI progress
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-03-fossil-tui-mvp.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-03-rethink-commit-selection.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-04-fossil-tui-mvp.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-03-rethink-commit-selection.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-03-fossil-tui-mvp.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-03-rethink-commit-selection.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-04-fossil-tui-mvp.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-03-rethink-commit-selection.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-03-fossil-tui-mvp.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-03-rethink-commit-selection.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-04-fossil-tui-mvp.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-03-rethink-commit-selection.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-03-fossil-tui-mvp.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-03-rethink-commit-selection.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-03-rethink-commit-selection.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-04-fossil-tui-mvp.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-03-fossil-tui-mvp.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-03-rethink-commit-selection.md ===
status: ok
stdout:
=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p .pi/plans/2026-06-04-fossil-tui-mvp.md ===
status: ok
stdout: