Skip to content

Commit 77dbba0

Browse files
authored
Merge pull request #8 from tspython/claude/integrate-difftastic-xF7FY
Integrate difftastic and add settings modal
2 parents b9ea6ac + 2539020 commit 77dbba0

2 files changed

Lines changed: 501 additions & 10 deletions

File tree

src/app.rs

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ enum InputMode {
6464
RepoPicker,
6565
DiscardConfirm,
6666
BranchSwitcher,
67+
Settings,
6768
}
6869

6970
struct State {
@@ -127,6 +128,9 @@ struct State {
127128
window_controls: Vec<WindowControlButton>,
128129
toolbar_buttons: Vec<ToolbarButton>,
129130

131+
// ── Settings modal ───────────────────────────────────
132+
settings_index: usize,
133+
130134
// ── Panel split ratio ────────────────────────────────
131135
file_pane_ratio: f32,
132136
divider_dragging: bool,
@@ -465,6 +469,7 @@ impl State {
465469
mouse_pos: PhysicalPosition::new(0.0, 0.0),
466470
window_controls: Vec::new(),
467471
toolbar_buttons: Vec::new(),
472+
settings_index: 0,
468473
file_pane_ratio: 0.30,
469474
divider_dragging: false,
470475
zoom_level: 1.0,
@@ -651,6 +656,15 @@ impl State {
651656
self.set_status(StatusKind::Prompt, message);
652657
}
653658

659+
fn prompt_settings(&mut self) {
660+
self.input_mode = InputMode::Settings;
661+
self.settings_index = 0;
662+
self.set_status(
663+
StatusKind::Prompt,
664+
"Settings: Up/Down navigate Left/Right change Esc close",
665+
);
666+
}
667+
654668
fn cancel_input_mode(&mut self) {
655669
self.input_mode = InputMode::Normal;
656670
self.commit_summary.clear();
@@ -1047,6 +1061,90 @@ impl State {
10471061
}
10481062
}
10491063

1064+
/// Total number of rows in the settings modal.
1065+
const SETTINGS_COUNT: usize = 3;
1066+
1067+
fn handle_settings_input(&mut self, key: &Key) -> anyhow::Result<bool> {
1068+
match key {
1069+
Key::Named(NamedKey::Escape) => {
1070+
self.cancel_input_mode();
1071+
Ok(true)
1072+
}
1073+
Key::Named(NamedKey::ArrowUp) => {
1074+
if self.settings_index > 0 {
1075+
self.settings_index -= 1;
1076+
}
1077+
Ok(true)
1078+
}
1079+
Key::Named(NamedKey::ArrowDown) => {
1080+
if self.settings_index + 1 < Self::SETTINGS_COUNT {
1081+
self.settings_index += 1;
1082+
}
1083+
Ok(true)
1084+
}
1085+
Key::Named(NamedKey::ArrowRight | NamedKey::Enter) => {
1086+
self.settings_adjust(1)?;
1087+
Ok(true)
1088+
}
1089+
Key::Named(NamedKey::ArrowLeft) => {
1090+
self.settings_adjust(-1)?;
1091+
Ok(true)
1092+
}
1093+
_ => Ok(false),
1094+
}
1095+
}
1096+
1097+
/// Adjust the setting at `self.settings_index` by the given direction.
1098+
fn settings_adjust(&mut self, direction: i32) -> anyhow::Result<()> {
1099+
match self.settings_index {
1100+
// 0 = Diff backend
1101+
0 => {
1102+
match self.git.toggle_diff_backend() {
1103+
Ok(_) => {
1104+
self.refresh_document_from_git()?;
1105+
}
1106+
Err(e) => {
1107+
self.set_status(StatusKind::Error, format!("{e}"));
1108+
}
1109+
}
1110+
}
1111+
// 1 = Pane ratio
1112+
1 => {
1113+
let delta = direction as f32 * 0.05;
1114+
self.adjust_pane_ratio(delta);
1115+
}
1116+
// 2 = Zoom level
1117+
2 => {
1118+
let delta = direction as f32 * 0.10;
1119+
self.apply_zoom(delta);
1120+
}
1121+
_ => {}
1122+
}
1123+
self.geometry_dirty = true;
1124+
Ok(())
1125+
}
1126+
1127+
/// Build the label and current value for each settings row.
1128+
fn settings_row_label(&self, index: usize) -> (&'static str, String) {
1129+
match index {
1130+
0 => (
1131+
"Diff backend",
1132+
self.git.diff_backend().label().to_string(),
1133+
),
1134+
1 => (
1135+
"Pane split",
1136+
format!("{:.0}% / {:.0}%",
1137+
self.file_pane_ratio * 100.0,
1138+
(1.0 - self.file_pane_ratio) * 100.0),
1139+
),
1140+
2 => (
1141+
"Zoom level",
1142+
format!("{:.0}%", self.zoom_level * 100.0),
1143+
),
1144+
_ => ("", String::new()),
1145+
}
1146+
}
1147+
10501148
fn execute_action<F>(&mut self, label: &str, action: F) -> bool
10511149
where
10521150
F: FnOnce(&mut Self) -> anyhow::Result<()>,
@@ -1239,6 +1337,9 @@ impl State {
12391337
InputMode::BranchSwitcher => {
12401338
self.build_branch_switcher_overlay_geometry(text_vertices, rect_instances)
12411339
}
1340+
InputMode::Settings => {
1341+
self.build_settings_overlay_geometry(text_vertices, rect_instances)
1342+
}
12421343
InputMode::Normal => Ok(()),
12431344
}
12441345
}
@@ -1643,6 +1744,115 @@ impl State {
16431744
Ok(())
16441745
}
16451746

1747+
fn build_settings_overlay_geometry(
1748+
&mut self,
1749+
text_vertices: &mut Vec<TextVertex>,
1750+
rect_instances: &mut Vec<StyledRectInstance>,
1751+
) -> anyhow::Result<()> {
1752+
let row_count = Self::SETTINGS_COUNT;
1753+
let panel_h =
1754+
self.ui(92.0) + row_count as f32 * (self.line_height + self.ui(6.0));
1755+
let panel = self.modal_panel_rect(panel_h);
1756+
push_styled_rect(
1757+
rect_instances,
1758+
panel,
1759+
theme::MODAL_BG_TOP,
1760+
theme::MODAL_BG_BOTTOM,
1761+
theme::MODAL_BORDER,
1762+
[0.0, 0.0, 0.0, 0.28],
1763+
self.ui(12.0),
1764+
1.0,
1765+
1.0,
1766+
self.ui(16.0),
1767+
[0.0, self.ui(4.0)],
1768+
self.ui(2.0),
1769+
);
1770+
1771+
// Title
1772+
let mut x = panel[0] + self.ui(16.0);
1773+
let mut y = panel[1] + self.ui(18.0);
1774+
self.append_text_run(
1775+
text_vertices,
1776+
&mut x,
1777+
y + self.ascent,
1778+
"Settings",
1779+
[0.92, 0.96, 1.0, 1.0],
1780+
)?;
1781+
1782+
y += self.line_height * 1.4;
1783+
let row_x = panel[0] + self.ui(14.0);
1784+
let row_w = panel[2] - self.ui(28.0);
1785+
1786+
for idx in 0..row_count {
1787+
let row_y = y + idx as f32 * (self.line_height + self.ui(6.0));
1788+
let selected = idx == self.settings_index;
1789+
1790+
let (fill_top, fill_bottom, stroke) = if selected {
1791+
(
1792+
[0.25, 0.33, 0.50, 1.0],
1793+
[0.18, 0.24, 0.38, 1.0],
1794+
[0.52, 0.68, 0.98, 0.70],
1795+
)
1796+
} else {
1797+
(
1798+
[0.16, 0.18, 0.26, 1.0],
1799+
[0.12, 0.14, 0.20, 1.0],
1800+
[0.30, 0.36, 0.48, 0.40],
1801+
)
1802+
};
1803+
1804+
push_styled_rect(
1805+
rect_instances,
1806+
[row_x, row_y, row_w, self.line_height + self.ui(4.0)],
1807+
fill_top,
1808+
fill_bottom,
1809+
stroke,
1810+
[0.0, 0.0, 0.0, 0.0],
1811+
self.ui(8.0),
1812+
1.0,
1813+
1.0,
1814+
0.0,
1815+
[0.0, 0.0],
1816+
0.0,
1817+
);
1818+
1819+
let (label, value) = self.settings_row_label(idx);
1820+
let arrows = if selected { "< > " } else { " " };
1821+
let display = format!("{label}: {arrows}{value}");
1822+
1823+
let text_color = if selected {
1824+
[1.0, 1.0, 1.0, 1.0]
1825+
} else {
1826+
[0.86, 0.90, 0.96, 1.0]
1827+
};
1828+
1829+
let mut tx = row_x + self.ui(10.0);
1830+
self.append_text_run(
1831+
text_vertices,
1832+
&mut tx,
1833+
row_y + self.ascent + 2.0,
1834+
&compact_status_message(
1835+
&display,
1836+
((row_w - self.ui(20.0)) / self.cell_width).max(1.0) as usize,
1837+
),
1838+
text_color,
1839+
)?;
1840+
}
1841+
1842+
// Hint bar
1843+
let mut hint_x = panel[0] + self.ui(16.0);
1844+
let hint_y = panel[1] + panel[3] - self.line_height + self.ui(4.0);
1845+
self.append_text_run(
1846+
text_vertices,
1847+
&mut hint_x,
1848+
hint_y + self.ascent,
1849+
"Left/Right change Up/Down navigate Esc close",
1850+
[0.76, 0.82, 0.92, 1.0],
1851+
)?;
1852+
1853+
Ok(())
1854+
}
1855+
16461856
fn refresh_document_from_git(&mut self) -> anyhow::Result<()> {
16471857
let (file_doc, meta, diff_doc) = self.git.build_split_documents()?;
16481858
let (file_line_to_index, file_index_to_line) =
@@ -3060,6 +3270,22 @@ impl State {
30603270
Ok(true)
30613271
}
30623272
},
3273+
"d" => {
3274+
Ok(self.execute_action("Diff backend toggled", |state| {
3275+
let backend = state.git.toggle_diff_backend()?;
3276+
state.refresh_document_from_git()?;
3277+
state.set_status(
3278+
StatusKind::Success,
3279+
format!("Diff backend: {}", backend.label()),
3280+
);
3281+
Ok(())
3282+
}))
3283+
}
3284+
"," => {
3285+
self.prompt_settings();
3286+
self.geometry_dirty = true;
3287+
Ok(true)
3288+
}
30633289
"r" => self.handle_toolbar_action(ToolbarAction::Refresh),
30643290
"s" => self.handle_toolbar_action(ToolbarAction::Stage),
30653291
"u" => self.handle_toolbar_action(ToolbarAction::Unstage),
@@ -3317,6 +3543,9 @@ impl ApplicationHandler for App {
33173543
InputMode::BranchSwitcher => {
33183544
st.handle_branch_switcher_input(&event.logical_key)
33193545
}
3546+
InputMode::Settings => {
3547+
st.handle_settings_input(&event.logical_key)
3548+
}
33203549
InputMode::Normal => Ok(false),
33213550
}
33223551
} else {
@@ -3350,6 +3579,7 @@ impl ApplicationHandler for App {
33503579
InputMode::RepoPicker => "Repo switch failed",
33513580
InputMode::DiscardConfirm => "Discard failed",
33523581
InputMode::BranchSwitcher => "Branch switch failed",
3582+
InputMode::Settings => "Settings failed",
33533583
InputMode::Normal => "Input handling failed",
33543584
};
33553585
st.set_status(StatusKind::Error, format!("{label}: {err}"));

0 commit comments

Comments
 (0)