=== 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:     88e4b4d1906491eb5c24ca9117f2520a9eee735f 2026-06-06 08:16:25 UTC
parent:       4c2f33e147f04978ac861294e56d7f7ac90da6e2 2026-06-06 08:14:17 UTC
tags:         trunk, v0.3.3
comment:      Bump version to 0.3.3 and update README (user: geraldo)
check-ins:    30


=== 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:     88e4b4d1906491eb5c24ca9117f2520a9eee735f 2026-06-06 08:16:25 UTC
parent:       4c2f33e147f04978ac861294e56d7f7ac90da6e2 2026-06-06 08:14:17 UTC
tags:         trunk, v0.3.3
comment:      Bump version to 0.3.3 and update README (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-06-2026-06-06-editor-discard-open-conflicts.md
../../NOTES.md
../../README.bak
../../crates.io-athena-token
lazyfossil_logo_2_high_res.png
screenshot_01.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:
88e4b4d190|geraldo|2026-06-06 08:16:25|Bump version to 0.3.3 and update README
4c2f33e147|geraldo|2026-06-06 08:14:17|Update README
40105961ed|geraldo|2026-06-06 08:02:23|Consolidate plans into master plan and archive older drafts
131a1b3e5c|geraldo|2026-06-06 07:47:18|Add badges
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
--- entry limit (20) reached ---


=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../README.md ===
status: ok
stdout:
88e4b4d190|geraldo|2026-06-06 08:16:25|Bump version to 0.3.3 and update README
4c2f33e147|geraldo|2026-06-06 08:14:17|Update README
131a1b3e5c|geraldo|2026-06-06 07:47:18|Add badges
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
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
+++ no more data (11) +++


=== fossil diff -- ../../README.md ===
status: ok
stdout:
Index: README.md
==================================================================
--- README.md
+++ README.md
@@ -1,11 +1,11 @@
 # lazyfossil
 
 [![Crates.io](https://img.shields.io/crates/v/lazyfossil)](https://crates.io/crates/lazyfossil)
 [![Multiplatform Release](https://github.com/geraldolsribeiro/lazyfossil/actions/workflows/release.yml/badge.svg)](https://github.com/geraldolsribeiro/lazyfossil/actions/workflows/release.yml)
-[![GitHub stars](https://img.shields.io/github/stars/geraldolsribeiro/lazyfossil)](https://github.com/geraldolsribeiro/lazyfossil/stargazers)
 [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
+[![GitHub stars](https://img.shields.io/github/stars/geraldolsribeiro/lazyfossil)](https://github.com/geraldolsribeiro/lazyfossil/stargazers)
 
 ![lazyfossil logo](doc/images/lazyfossil_logo.png)
 
 A lazygit-inspired terminal UI for Fossil SCM.
 



=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../src/app.rs ===
status: ok
stdout:
0a0984b16d|geraldo|2026-06-06 07:26:16|Bump version to 0.3.2 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
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
+++ no more data (11) +++


=== fossil diff -- ../../src/app.rs ===
status: ok
stdout:
Index: src/app.rs
==================================================================
--- src/app.rs
+++ src/app.rs
@@ -4,13 +4,15 @@
 use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, MouseEventKind};
 use crossterm::execute;
 use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
 use ratatui::backend::CrosstermBackend;
 use ratatui::Terminal;
+use std::env;
 use std::fs;
 use std::io;
 use std::path::Path;
+use std::process::Command;
 
 const ASCII_LOGO: &str = include_str!("../doc/images/lazyfossil_logo_01.txt");
 use std::time::Duration;
 
 pub fn run() -> Result<()> {
@@ -158,10 +160,50 @@
     }
 
     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())
     }
+
+    fn open_in_editor(&mut self) {
+        let Some(path) = self.current_file_path() else { return; };
+        let Some(editor) = env::var("EDITOR").ok() else {
+            self.state.error = Some("EDITOR is not defined".to_string());
+            return;
+        };
+        if let Err(err) = self.spawn_external(&editor, &[path.as_str()]) {
+            self.state.error = Some(err);
+        }
+    }
+
+    fn discard_current_file(&mut self) {
+        let Some(path) = self.current_file_path() else { return; };
+        match self.client.discard_file(&path) {
+            Ok(_) => self.refresh(),
+            Err(err) => self.state.error = Some(err.to_string()),
+        }
+    }
+
+    fn open_selected_file(&mut self) {
+        let Some(path) = self.current_file_path() else { return; };
+        let Some(cmd) = open_command_for(&path) else {
+            self.state.error = Some("No app configured for this file type".to_string());
+            return;
+        };
+        let args = if cmd == "xdg-open" { vec![path.as_str()] } else { vec![path.as_str()] };
+        if let Err(err) = self.spawn_external(&cmd, &args) {
+            self.state.error = Some(err);
+        }
+    }
+
+    fn spawn_external(&self, program: &str, args: &[&str]) -> std::result::Result<(), String> {
+        disable_raw_mode().map_err(|e| e.to_string())?;
+        let _ = execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture);
+        let status = Command::new(program).args(args).status().map_err(|e| e.to_string())?;
+        let _ = execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture);
+        enable_raw_mode().map_err(|e| e.to_string())?;
+        if status.success() { Ok(()) } else { Err(format!("{} exited with {}", program, status)) }
+    }
 
     fn toggle_selected_file(&mut self) {
         let Some(path) = self.current_file_path() else { return; };
         if let Some(pos) = self.state.selected_files.iter().position(|p| p == &path) {
             self.state.selected_files.remove(pos);
@@ -306,10 +348,13 @@
                             KeyCode::Char(' ') => self.toggle_selected_file(),
                             KeyCode::Char('c') => self.start_commit(CommitTarget::Selected),
                             KeyCode::Char('f') => self.start_commit(CommitTarget::Current),
                             KeyCode::Char('a') => self.start_commit(CommitTarget::All),
                             KeyCode::Char('i') => self.start_ignore(),
+                            KeyCode::Char('e') => self.open_in_editor(),
+                            KeyCode::Char('d') => self.discard_current_file(),
+                            KeyCode::Char('o') => self.open_selected_file(),
                             KeyCode::Tab => {
                                 self.state.tab = match self.state.tab { Tab::WorkingTree => Tab::History, Tab::History => Tab::WorkingTree };
                                 self.refresh_history();
                             }
                             _ => {}
@@ -331,10 +376,21 @@
 
 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))
 }
+
+fn open_command_for(path: &str) -> Option<String> {
+    let ext = Path::new(path).extension()?.to_str()?.to_ascii_lowercase();
+    let cmd = match ext.as_str() {
+        "txt" | "md" | "rs" | "toml" | "log" => env::var("EDITOR").unwrap_or_else(|_| "vi".to_string()),
+        "png" | "jpg" | "jpeg" | "gif" | "ico" => "xdg-open".to_string(),
+        "pdf" => "xdg-open".to_string(),
+        _ => "xdg-open".to_string(),
+    };
+    Some(cmd)
+}
 
 #[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:
0a0984b16d|geraldo|2026-06-06 07:26:16|Bump version to 0.3.2 and rebuild
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
+++ no more data (11) +++


=== fossil diff -- ../../src/fossil.rs ===
status: ok
stdout:
Index: src/fossil.rs
==================================================================
--- src/fossil.rs
+++ 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 discard_file(&self, path: &str) -> std::result::Result<String, FossilError> {
+        self.run(&["revert", "--", path])
+    }
 
     pub fn set_binary_glob(&self, pattern: &str) -> std::result::Result<String, FossilError> {
         self.run(&["settings", "binary-glob", pattern])
     }
 
@@ -200,10 +204,12 @@
             line.strip_prefix("EDITED ")
                 .map(|path| FileStatus { path: path.trim().to_string(), status: "edited".to_string() })
                 .or_else(|| line.strip_prefix("ADDED   ").map(|path| FileStatus { path: path.trim().to_string(), status: "added".to_string() }))
                 .or_else(|| line.strip_prefix("DELETED ").map(|path| FileStatus { path: path.trim().to_string(), status: "deleted".to_string() }))
                 .or_else(|| line.strip_prefix("CHECKED-OUT ").map(|path| FileStatus { path: path.trim().to_string(), status: "checked-out".to_string() }))
+                .or_else(|| line.strip_prefix("CONFLICT ").map(|path| FileStatus { path: path.trim().to_string(), status: "conflict".to_string() }))
+                .or_else(|| line.strip_prefix("MERGE-CONFLICT ").map(|path| FileStatus { path: path.trim().to_string(), status: "conflict".to_string() }))
         })
         .collect()
 }
 
 fn parse_extra(out: &str) -> Vec<FileStatus> {



=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../src/ui.rs ===
status: ok
stdout:
0a0984b16d|geraldo|2026-06-06 07:26:16|Bump version to 0.3.2 and rebuild
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
+++ no more data (12) +++


=== fossil diff -- ../../src/ui.rs ===
status: ok
stdout:
Index: src/ui.rs
==================================================================
--- src/ui.rs
+++ src/ui.rs
@@ -23,17 +23,23 @@
 
     let mut file_state = ListState::default();
     let left = if let Some(repo) = &state.repo {
         file_state.select(Some(repo.selected_file));
         let items: Vec<ListItem> = repo.files.iter().enumerate().map(|(i, f)| {
-            let prefix = if i == repo.selected_file { ">" } else { " " };
             let selected = if state.selected_files.iter().any(|p| p == &f.path) { "*" } else { " " };
-            let kind = match f.status.as_str() { "extra" => "E", "edited" => "M", "added" => "A", "deleted" => "D", _ => "?" };
-            ListItem::new(format!("{}{} {} {}", prefix, selected, kind, f.path))
+            let kind = match f.status.as_str() { "extra" => "??", "edited" => "M", "added" => "A", "deleted" => "D", "conflict" => "C", _ => "?" };
+            let mut item = ListItem::new(format!("{} {}", kind, f.path));
+            if i == repo.selected_file {
+                item = item.style(Style::default().bg(Color::DarkGray));
+            }
+            if selected == "*" {
+                item = item.style(Style::default().add_modifier(Modifier::BOLD));
+            }
+            item
         }).collect();
         List::new(items)
-            .highlight_style(Style::default().add_modifier(Modifier::REVERSED))
+            .highlight_style(Style::default())
             .block(Block::default().borders(Borders::ALL).title("Files"))
     } else {
         List::new(vec![ListItem::new("No repository detected")])
             .block(Block::default().borders(Borders::ALL).title("Files"))
     };
@@ -101,11 +107,11 @@
         cursor = Some((areas[2].x + 1 + 7 + path.chars().count() as u16, areas[2].y + 1));
         Paragraph::new(text).block(Block::default().borders(Borders::ALL).title("Ignore"))
     } else {
         let sel_count = state.selected_files.len();
         let base = if let Some(repo) = &state.repo {
-            repo.files.get(repo.selected_file).map(|f| format!("q quit  r refresh  p/P sync  space toggle  c commit selected  f commit file  a commit all  i ignore  tab switch view | selected: {} [{}]", f.path, f.status)).unwrap_or_else(|| "q quit  r refresh  p/P sync  space toggle  c commit selected  f commit file  a commit all  i ignore  tab switch view".to_string())
+            repo.files.get(repo.selected_file).map(|f| format!("q quit  r refresh  p/P sync  e edit  d discard  o open  space toggle  c commit selected  f commit file  a commit all  i ignore  tab switch view | selected: {} [{}]", f.path, f.status)).unwrap_or_else(|| "q quit  r refresh  p/P sync  e edit  d discard  o open  space toggle  c commit selected  f commit file  a commit all  i ignore  tab switch view".to_string())
         } else {
             "q quit  r refresh  p/P sync  space toggle  c commit selected  f commit file  a commit all  i ignore  tab switch view".to_string()
         };
         Paragraph::new(format!("{}\nselected: {}", base, sel_count)).block(Block::default().borders(Borders::TOP))
     };



=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../.pi/plans/2026-06-06-2026-06-06-editor-discard-open-conflicts.md ===
status: ok
stdout:
+++ no more data (0) +++


=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../NOTES.md ===
status: ok
stdout:
+++ no more data (0) +++


=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../README.bak ===
status: ok
stdout:
+++ no more data (0) +++


=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../crates.io-athena-token ===
status: ok
stdout:
+++ no more data (0) +++


=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p lazyfossil_logo_2_high_res.png ===
status: ok
stdout:
+++ no more data (0) +++


=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p screenshot_01.png ===
status: ok
stdout:
+++ no more data (0) +++


=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p lazyfossil_logo_2_high_res.png ===
status: ok
stdout:
+++ no more data (0) +++


=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../crates.io-athena-token ===
status: ok
stdout:
+++ no more data (0) +++


=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../README.bak ===
status: ok
stdout:
+++ no more data (0) +++


=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../crates.io-athena-token ===
status: ok
stdout:
+++ no more data (0) +++


=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p lazyfossil_logo_2_high_res.png ===
status: ok
stdout:
+++ no more data (0) +++


=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p screenshot_01.png ===
status: ok
stdout:
+++ no more data (0) +++


=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p lazyfossil_logo_2_high_res.png ===
status: ok
stdout:
+++ no more data (0) +++


=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../crates.io-athena-token ===
status: ok
stdout:
+++ no more data (0) +++


=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../README.bak ===
status: ok
stdout:
+++ no more data (0) +++


=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../NOTES.md ===
status: ok
stdout:
+++ no more data (0) +++


=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../.pi/plans/2026-06-06-2026-06-06-editor-discard-open-conflicts.md ===
status: ok
stdout:
+++ no more data (0) +++


=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../src/ui.rs ===
status: ok
stdout:
0a0984b16d|geraldo|2026-06-06 07:26:16|Bump version to 0.3.2 and rebuild
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
+++ no more data (12) +++


=== fossil diff -- ../../src/ui.rs ===
status: ok
stdout:
Index: src/ui.rs
==================================================================
--- src/ui.rs
+++ src/ui.rs
@@ -23,17 +23,23 @@
 
     let mut file_state = ListState::default();
     let left = if let Some(repo) = &state.repo {
         file_state.select(Some(repo.selected_file));
         let items: Vec<ListItem> = repo.files.iter().enumerate().map(|(i, f)| {
-            let prefix = if i == repo.selected_file { ">" } else { " " };
             let selected = if state.selected_files.iter().any(|p| p == &f.path) { "*" } else { " " };
-            let kind = match f.status.as_str() { "extra" => "E", "edited" => "M", "added" => "A", "deleted" => "D", _ => "?" };
-            ListItem::new(format!("{}{} {} {}", prefix, selected, kind, f.path))
+            let kind = match f.status.as_str() { "extra" => "??", "edited" => "M", "added" => "A", "deleted" => "D", "conflict" => "C", _ => "?" };
+            let mut item = ListItem::new(format!("{} {}", kind, f.path));
+            if i == repo.selected_file {
+                item = item.style(Style::default().bg(Color::DarkGray));
+            }
+            if selected == "*" {
+                item = item.style(Style::default().add_modifier(Modifier::BOLD));
+            }
+            item
         }).collect();
         List::new(items)
-            .highlight_style(Style::default().add_modifier(Modifier::REVERSED))
+            .highlight_style(Style::default())
             .block(Block::default().borders(Borders::ALL).title("Files"))
     } else {
         List::new(vec![ListItem::new("No repository detected")])
             .block(Block::default().borders(Borders::ALL).title("Files"))
     };
@@ -101,11 +107,11 @@
         cursor = Some((areas[2].x + 1 + 7 + path.chars().count() as u16, areas[2].y + 1));
         Paragraph::new(text).block(Block::default().borders(Borders::ALL).title("Ignore"))
     } else {
         let sel_count = state.selected_files.len();
         let base = if let Some(repo) = &state.repo {
-            repo.files.get(repo.selected_file).map(|f| format!("q quit  r refresh  p/P sync  space toggle  c commit selected  f commit file  a commit all  i ignore  tab switch view | selected: {} [{}]", f.path, f.status)).unwrap_or_else(|| "q quit  r refresh  p/P sync  space toggle  c commit selected  f commit file  a commit all  i ignore  tab switch view".to_string())
+            repo.files.get(repo.selected_file).map(|f| format!("q quit  r refresh  p/P sync  e edit  d discard  o open  space toggle  c commit selected  f commit file  a commit all  i ignore  tab switch view | selected: {} [{}]", f.path, f.status)).unwrap_or_else(|| "q quit  r refresh  p/P sync  e edit  d discard  o open  space toggle  c commit selected  f commit file  a commit all  i ignore  tab switch view".to_string())
         } else {
             "q quit  r refresh  p/P sync  space toggle  c commit selected  f commit file  a commit all  i ignore  tab switch view".to_string()
         };
         Paragraph::new(format!("{}\nselected: {}", base, sel_count)).block(Block::default().borders(Borders::TOP))
     };



=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../src/fossil.rs ===
status: ok
stdout:
0a0984b16d|geraldo|2026-06-06 07:26:16|Bump version to 0.3.2 and rebuild
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
+++ no more data (11) +++


=== fossil diff -- ../../src/fossil.rs ===
status: ok
stdout:
Index: src/fossil.rs
==================================================================
--- src/fossil.rs
+++ 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 discard_file(&self, path: &str) -> std::result::Result<String, FossilError> {
+        self.run(&["revert", "--", path])
+    }
 
     pub fn set_binary_glob(&self, pattern: &str) -> std::result::Result<String, FossilError> {
         self.run(&["settings", "binary-glob", pattern])
     }
 
@@ -200,10 +204,12 @@
             line.strip_prefix("EDITED ")
                 .map(|path| FileStatus { path: path.trim().to_string(), status: "edited".to_string() })
                 .or_else(|| line.strip_prefix("ADDED   ").map(|path| FileStatus { path: path.trim().to_string(), status: "added".to_string() }))
                 .or_else(|| line.strip_prefix("DELETED ").map(|path| FileStatus { path: path.trim().to_string(), status: "deleted".to_string() }))
                 .or_else(|| line.strip_prefix("CHECKED-OUT ").map(|path| FileStatus { path: path.trim().to_string(), status: "checked-out".to_string() }))
+                .or_else(|| line.strip_prefix("CONFLICT ").map(|path| FileStatus { path: path.trim().to_string(), status: "conflict".to_string() }))
+                .or_else(|| line.strip_prefix("MERGE-CONFLICT ").map(|path| FileStatus { path: path.trim().to_string(), status: "conflict".to_string() }))
         })
         .collect()
 }
 
 fn parse_extra(out: &str) -> Vec<FileStatus> {



=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../src/app.rs ===
status: ok
stdout:
0a0984b16d|geraldo|2026-06-06 07:26:16|Bump version to 0.3.2 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
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
+++ no more data (11) +++


=== fossil diff -- ../../src/app.rs ===
status: ok
stdout:
Index: src/app.rs
==================================================================
--- src/app.rs
+++ src/app.rs
@@ -4,13 +4,15 @@
 use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, MouseEventKind};
 use crossterm::execute;
 use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
 use ratatui::backend::CrosstermBackend;
 use ratatui::Terminal;
+use std::env;
 use std::fs;
 use std::io;
 use std::path::Path;
+use std::process::Command;
 
 const ASCII_LOGO: &str = include_str!("../doc/images/lazyfossil_logo_01.txt");
 use std::time::Duration;
 
 pub fn run() -> Result<()> {
@@ -158,10 +160,50 @@
     }
 
     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())
     }
+
+    fn open_in_editor(&mut self) {
+        let Some(path) = self.current_file_path() else { return; };
+        let Some(editor) = env::var("EDITOR").ok() else {
+            self.state.error = Some("EDITOR is not defined".to_string());
+            return;
+        };
+        if let Err(err) = self.spawn_external(&editor, &[path.as_str()]) {
+            self.state.error = Some(err);
+        }
+    }
+
+    fn discard_current_file(&mut self) {
+        let Some(path) = self.current_file_path() else { return; };
+        match self.client.discard_file(&path) {
+            Ok(_) => self.refresh(),
+            Err(err) => self.state.error = Some(err.to_string()),
+        }
+    }
+
+    fn open_selected_file(&mut self) {
+        let Some(path) = self.current_file_path() else { return; };
+        let Some(cmd) = open_command_for(&path) else {
+            self.state.error = Some("No app configured for this file type".to_string());
+            return;
+        };
+        let args = if cmd == "xdg-open" { vec![path.as_str()] } else { vec![path.as_str()] };
+        if let Err(err) = self.spawn_external(&cmd, &args) {
+            self.state.error = Some(err);
+        }
+    }
+
+    fn spawn_external(&self, program: &str, args: &[&str]) -> std::result::Result<(), String> {
+        disable_raw_mode().map_err(|e| e.to_string())?;
+        let _ = execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture);
+        let status = Command::new(program).args(args).status().map_err(|e| e.to_string())?;
+        let _ = execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture);
+        enable_raw_mode().map_err(|e| e.to_string())?;
+        if status.success() { Ok(()) } else { Err(format!("{} exited with {}", program, status)) }
+    }
 
     fn toggle_selected_file(&mut self) {
         let Some(path) = self.current_file_path() else { return; };
         if let Some(pos) = self.state.selected_files.iter().position(|p| p == &path) {
             self.state.selected_files.remove(pos);
@@ -306,10 +348,13 @@
                             KeyCode::Char(' ') => self.toggle_selected_file(),
                             KeyCode::Char('c') => self.start_commit(CommitTarget::Selected),
                             KeyCode::Char('f') => self.start_commit(CommitTarget::Current),
                             KeyCode::Char('a') => self.start_commit(CommitTarget::All),
                             KeyCode::Char('i') => self.start_ignore(),
+                            KeyCode::Char('e') => self.open_in_editor(),
+                            KeyCode::Char('d') => self.discard_current_file(),
+                            KeyCode::Char('o') => self.open_selected_file(),
                             KeyCode::Tab => {
                                 self.state.tab = match self.state.tab { Tab::WorkingTree => Tab::History, Tab::History => Tab::WorkingTree };
                                 self.refresh_history();
                             }
                             _ => {}
@@ -331,10 +376,21 @@
 
 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))
 }
+
+fn open_command_for(path: &str) -> Option<String> {
+    let ext = Path::new(path).extension()?.to_str()?.to_ascii_lowercase();
+    let cmd = match ext.as_str() {
+        "txt" | "md" | "rs" | "toml" | "log" => env::var("EDITOR").unwrap_or_else(|_| "vi".to_string()),
+        "png" | "jpg" | "jpeg" | "gif" | "ico" => "xdg-open".to_string(),
+        "pdf" => "xdg-open".to_string(),
+        _ => "xdg-open".to_string(),
+    };
+    Some(cmd)
+}
 
 #[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:
88e4b4d190|geraldo|2026-06-06 08:16:25|Bump version to 0.3.3 and update README
4c2f33e147|geraldo|2026-06-06 08:14:17|Update README
131a1b3e5c|geraldo|2026-06-06 07:47:18|Add badges
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
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
+++ no more data (11) +++


=== fossil diff -- ../../README.md ===
status: ok
stdout:
Index: README.md
==================================================================
--- README.md
+++ README.md
@@ -1,11 +1,11 @@
 # lazyfossil
 
 [![Crates.io](https://img.shields.io/crates/v/lazyfossil)](https://crates.io/crates/lazyfossil)
 [![Multiplatform Release](https://github.com/geraldolsribeiro/lazyfossil/actions/workflows/release.yml/badge.svg)](https://github.com/geraldolsribeiro/lazyfossil/actions/workflows/release.yml)
-[![GitHub stars](https://img.shields.io/github/stars/geraldolsribeiro/lazyfossil)](https://github.com/geraldolsribeiro/lazyfossil/stargazers)
 [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
+[![GitHub stars](https://img.shields.io/github/stars/geraldolsribeiro/lazyfossil)](https://github.com/geraldolsribeiro/lazyfossil/stargazers)
 
 ![lazyfossil logo](doc/images/lazyfossil_logo.png)
 
 A lazygit-inspired terminal UI for Fossil SCM.
 



=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../README.md ===
status: ok
stdout:
88e4b4d190|geraldo|2026-06-06 08:16:25|Bump version to 0.3.3 and update README
4c2f33e147|geraldo|2026-06-06 08:14:17|Update README
131a1b3e5c|geraldo|2026-06-06 07:47:18|Add badges
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
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
+++ no more data (11) +++


=== fossil diff -- ../../README.md ===
status: ok
stdout:
Index: README.md
==================================================================
--- README.md
+++ README.md
@@ -1,11 +1,11 @@
 # lazyfossil
 
 [![Crates.io](https://img.shields.io/crates/v/lazyfossil)](https://crates.io/crates/lazyfossil)
 [![Multiplatform Release](https://github.com/geraldolsribeiro/lazyfossil/actions/workflows/release.yml/badge.svg)](https://github.com/geraldolsribeiro/lazyfossil/actions/workflows/release.yml)
-[![GitHub stars](https://img.shields.io/github/stars/geraldolsribeiro/lazyfossil)](https://github.com/geraldolsribeiro/lazyfossil/stargazers)
 [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
+[![GitHub stars](https://img.shields.io/github/stars/geraldolsribeiro/lazyfossil)](https://github.com/geraldolsribeiro/lazyfossil/stargazers)
 
 ![lazyfossil logo](doc/images/lazyfossil_logo.png)
 
 A lazygit-inspired terminal UI for Fossil SCM.
 



=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../README.md ===
status: ok
stdout:
88e4b4d190|geraldo|2026-06-06 08:16:25|Bump version to 0.3.3 and update README
4c2f33e147|geraldo|2026-06-06 08:14:17|Update README
131a1b3e5c|geraldo|2026-06-06 07:47:18|Add badges
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
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
+++ no more data (11) +++


=== fossil diff -- ../../README.md ===
status: ok
stdout:
Index: README.md
==================================================================
--- README.md
+++ README.md
@@ -1,11 +1,11 @@
 # lazyfossil
 
 [![Crates.io](https://img.shields.io/crates/v/lazyfossil)](https://crates.io/crates/lazyfossil)
 [![Multiplatform Release](https://github.com/geraldolsribeiro/lazyfossil/actions/workflows/release.yml/badge.svg)](https://github.com/geraldolsribeiro/lazyfossil/actions/workflows/release.yml)
-[![GitHub stars](https://img.shields.io/github/stars/geraldolsribeiro/lazyfossil)](https://github.com/geraldolsribeiro/lazyfossil/stargazers)
 [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
+[![GitHub stars](https://img.shields.io/github/stars/geraldolsribeiro/lazyfossil)](https://github.com/geraldolsribeiro/lazyfossil/stargazers)
 
 ![lazyfossil logo](doc/images/lazyfossil_logo.png)
 
 A lazygit-inspired terminal UI for Fossil SCM.
 



=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../README.md ===
status: ok
stdout:
88e4b4d190|geraldo|2026-06-06 08:16:25|Bump version to 0.3.3 and update README
4c2f33e147|geraldo|2026-06-06 08:14:17|Update README
131a1b3e5c|geraldo|2026-06-06 07:47:18|Add badges
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
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
+++ no more data (11) +++


=== fossil diff -- ../../README.md ===
status: ok
stdout:
Index: README.md
==================================================================
--- README.md
+++ README.md
@@ -1,11 +1,11 @@
 # lazyfossil
 
 [![Crates.io](https://img.shields.io/crates/v/lazyfossil)](https://crates.io/crates/lazyfossil)
 [![Multiplatform Release](https://github.com/geraldolsribeiro/lazyfossil/actions/workflows/release.yml/badge.svg)](https://github.com/geraldolsribeiro/lazyfossil/actions/workflows/release.yml)
-[![GitHub stars](https://img.shields.io/github/stars/geraldolsribeiro/lazyfossil)](https://github.com/geraldolsribeiro/lazyfossil/stargazers)
 [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
+[![GitHub stars](https://img.shields.io/github/stars/geraldolsribeiro/lazyfossil)](https://github.com/geraldolsribeiro/lazyfossil/stargazers)
 
 ![lazyfossil logo](doc/images/lazyfossil_logo.png)
 
 A lazygit-inspired terminal UI for Fossil SCM.
 



=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../README.md ===
status: ok
stdout:
88e4b4d190|geraldo|2026-06-06 08:16:25|Bump version to 0.3.3 and update README
4c2f33e147|geraldo|2026-06-06 08:14:17|Update README
131a1b3e5c|geraldo|2026-06-06 07:47:18|Add badges
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
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
+++ no more data (11) +++


=== fossil diff -- ../../README.md ===
status: ok
stdout:
Index: README.md
==================================================================
--- README.md
+++ README.md
@@ -1,11 +1,11 @@
 # lazyfossil
 
 [![Crates.io](https://img.shields.io/crates/v/lazyfossil)](https://crates.io/crates/lazyfossil)
 [![Multiplatform Release](https://github.com/geraldolsribeiro/lazyfossil/actions/workflows/release.yml/badge.svg)](https://github.com/geraldolsribeiro/lazyfossil/actions/workflows/release.yml)
-[![GitHub stars](https://img.shields.io/github/stars/geraldolsribeiro/lazyfossil)](https://github.com/geraldolsribeiro/lazyfossil/stargazers)
 [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
+[![GitHub stars](https://img.shields.io/github/stars/geraldolsribeiro/lazyfossil)](https://github.com/geraldolsribeiro/lazyfossil/stargazers)
 
 ![lazyfossil logo](doc/images/lazyfossil_logo.png)
 
 A lazygit-inspired terminal UI for Fossil SCM.
 



=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../README.md ===
status: ok
stdout:
88e4b4d190|geraldo|2026-06-06 08:16:25|Bump version to 0.3.3 and update README
4c2f33e147|geraldo|2026-06-06 08:14:17|Update README
131a1b3e5c|geraldo|2026-06-06 07:47:18|Add badges
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
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
+++ no more data (11) +++


=== fossil diff -- ../../README.md ===
status: ok
stdout:
Index: README.md
==================================================================
--- README.md
+++ README.md
@@ -1,11 +1,11 @@
 # lazyfossil
 
 [![Crates.io](https://img.shields.io/crates/v/lazyfossil)](https://crates.io/crates/lazyfossil)
 [![Multiplatform Release](https://github.com/geraldolsribeiro/lazyfossil/actions/workflows/release.yml/badge.svg)](https://github.com/geraldolsribeiro/lazyfossil/actions/workflows/release.yml)
-[![GitHub stars](https://img.shields.io/github/stars/geraldolsribeiro/lazyfossil)](https://github.com/geraldolsribeiro/lazyfossil/stargazers)
 [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
+[![GitHub stars](https://img.shields.io/github/stars/geraldolsribeiro/lazyfossil)](https://github.com/geraldolsribeiro/lazyfossil/stargazers)
 
 ![lazyfossil logo](doc/images/lazyfossil_logo.png)
 
 A lazygit-inspired terminal UI for Fossil SCM.
 



=== fossil timeline -n 20 -t ci -F %h|%a|%d|%c -p ../../src/app.rs ===
status: ok
stdout:
0a0984b16d|geraldo|2026-06-06 07:26:16|Bump version to 0.3.2 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
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
+++ no more data (11) +++


=== fossil diff -- ../../src/app.rs ===
status: ok
stdout:
Index: src/app.rs
==================================================================
--- src/app.rs
+++ src/app.rs
@@ -4,13 +4,15 @@
 use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, MouseEventKind};
 use crossterm::execute;
 use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
 use ratatui::backend::CrosstermBackend;
 use ratatui::Terminal;
+use std::env;
 use std::fs;
 use std::io;
 use std::path::Path;
+use std::process::Command;
 
 const ASCII_LOGO: &str = include_str!("../doc/images/lazyfossil_logo_01.txt");
 use std::time::Duration;
 
 pub fn run() -> Result<()> {
@@ -158,10 +160,50 @@
     }
 
     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())
     }
+
+    fn open_in_editor(&mut self) {
+        let Some(path) = self.current_file_path() else { return; };
+        let Some(editor) = env::var("EDITOR").ok() else {
+            self.state.error = Some("EDITOR is not defined".to_string());
+            return;
+        };
+        if let Err(err) = self.spawn_external(&editor, &[path.as_str()]) {
+            self.state.error = Some(err);
+        }
+    }
+
+    fn discard_current_file(&mut self) {
+        let Some(path) = self.current_file_path() else { return; };
+        match self.client.discard_file(&path) {
+            Ok(_) => self.refresh(),
+            Err(err) => self.state.error = Some(err.to_string()),
+        }
+    }
+
+    fn open_selected_file(&mut self) {
+        let Some(path) = self.current_file_path() else { return; };
+        let Some(cmd) = open_command_for(&path) else {
+            self.state.error = Some("No app configured for this file type".to_string());
+            return;
+        };
+        let args = if cmd == "xdg-open" { vec![path.as_str()] } else { vec![path.as_str()] };
+        if let Err(err) = self.spawn_external(&cmd, &args) {
+            self.state.error = Some(err);
+        }
+    }
+
+    fn spawn_external(&self, program: &str, args: &[&str]) -> std::result::Result<(), String> {
+        disable_raw_mode().map_err(|e| e.to_string())?;
+        let _ = execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture);
+        let status = Command::new(program).args(args).status().map_err(|e| e.to_string())?;
+        let _ = execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture);
+        enable_raw_mode().map_err(|e| e.to_string())?;
+        if status.success() { Ok(()) } else { Err(format!("{} exited with {}", program, status)) }
+    }
 
     fn toggle_selected_file(&mut self) {
         let Some(path) = self.current_file_path() else { return; };
         if let Some(pos) = self.state.selected_files.iter().position(|p| p == &path) {
             self.state.selected_files.remove(pos);
@@ -306,10 +348,13 @@
                             KeyCode::Char(' ') => self.toggle_selected_file(),
                             KeyCode::Char('c') => self.start_commit(CommitTarget::Selected),
                             KeyCode::Char('f') => self.start_commit(CommitTarget::Current),
                             KeyCode::Char('a') => self.start_commit(CommitTarget::All),
                             KeyCode::Char('i') => self.start_ignore(),
+                            KeyCode::Char('e') => self.open_in_editor(),
+                            KeyCode::Char('d') => self.discard_current_file(),
+                            KeyCode::Char('o') => self.open_selected_file(),
                             KeyCode::Tab => {
                                 self.state.tab = match self.state.tab { Tab::WorkingTree => Tab::History, Tab::History => Tab::WorkingTree };
                                 self.refresh_history();
                             }
                             _ => {}
@@ -331,10 +376,21 @@
 
 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))
 }
+
+fn open_command_for(path: &str) -> Option<String> {
+    let ext = Path::new(path).extension()?.to_str()?.to_ascii_lowercase();
+    let cmd = match ext.as_str() {
+        "txt" | "md" | "rs" | "toml" | "log" => env::var("EDITOR").unwrap_or_else(|_| "vi".to_string()),
+        "png" | "jpg" | "jpeg" | "gif" | "ico" => "xdg-open".to_string(),
+        "pdf" => "xdg-open".to_string(),
+        _ => "xdg-open".to_string(),
+    };
+    Some(cmd)
+}
 
 #[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:
0a0984b16d|geraldo|2026-06-06 07:26:16|Bump version to 0.3.2 and rebuild
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
+++ no more data (11) +++


=== fossil diff -- ../../src/fossil.rs ===
status: ok
stdout:
Index: src/fossil.rs
==================================================================
--- src/fossil.rs
+++ 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 discard_file(&self, path: &str) -> std::result::Result<String, FossilError> {
+        self.run(&["revert", "--", path])
+    }
 
     pub fn set_binary_glob(&self, pattern: &str) -> std::result::Result<String, FossilError> {
         self.run(&["settings", "binary-glob", pattern])
     }
 
@@ -200,10 +204,12 @@
             line.strip_prefix("EDITED ")
                 .map(|path| FileStatus { path: path.trim().to_string(), status: "edited".to_string() })
                 .or_else(|| line.strip_prefix("ADDED   ").map(|path| FileStatus { path: path.trim().to_string(), status: "added".to_string() }))
                 .or_else(|| line.strip_prefix("DELETED ").map(|path| FileStatus { path: path.trim().to_string(), status: "deleted".to_string() }))
                 .or_else(|| line.strip_prefix("CHECKED-OUT ").map(|path| FileStatus { path: path.trim().to_string(), status: "checked-out".to_string() }))
+                .or_else(|| line.strip_prefix("CONFLICT ").map(|path| FileStatus { path: path.trim().to_string(), status: "conflict".to_string() }))
+                .or_else(|| line.strip_prefix("MERGE-CONFLICT ").map(|path| FileStatus { path: path.trim().to_string(), status: "conflict".to_string() }))
         })
         .collect()
 }
 
 fn parse_extra(out: &str) -> Vec<FileStatus> {



