Skip to content

Commit 712c97a

Browse files
committed
fix: improve tui markdown scroll and mouse
1 parent 10feca2 commit 712c97a

7 files changed

Lines changed: 425 additions & 338 deletions

File tree

Cargo.lock

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ sha2 = "0.11.0"
2525
thiserror = "2.0.18"
2626
ureq = { version = "3.3.0", features = ["json", "platform-verifier"] }
2727
url = "2.5.8"
28+
pulldown-cmark = { version = "0.13.4", default-features = false }

src/lib.rs

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,7 @@ impl App {
662662

663663
self.note_input.clear();
664664
self.notes.clear();
665+
self.editor_description_scroll = 0;
665666
self.notes_session_id = self.notes_session_id.wrapping_add(1);
666667
self.note_submit_pending = None;
667668
self.notes_issue_iid = Some(iid);
@@ -865,7 +866,7 @@ impl App {
865866
pub fn scroll_editor_description(&mut self, delta: i16) {
866867
if !matches!(
867868
self.overlay,
868-
Some(Overlay::Details | Overlay::EditIssue | Overlay::NewIssue)
869+
Some(Overlay::Details | Overlay::EditIssue | Overlay::NewIssue | Overlay::Notes)
869870
) {
870871
return;
871872
}
@@ -881,6 +882,12 @@ impl App {
881882
}
882883

883884
fn editor_description_scroll_max(&self) -> usize {
885+
if self.overlay == Some(Overlay::Notes) {
886+
let notes_lines = self.notes.len().clamp(1, 5);
887+
let input_lines = self.note_input.lines().count().max(1);
888+
return (notes_lines + input_lines + 6).saturating_sub(1);
889+
}
890+
884891
// The read-only details view always renders the markdown preview.
885892
let preview = self.editor_preview || self.overlay == Some(Overlay::Details);
886893
crate::ui::markdown::description_lines(
@@ -936,7 +943,13 @@ impl App {
936943
if self.overlay != Some(Overlay::GenerateStories) {
937944
return;
938945
}
939-
let max_scroll = self.story_source.lines().count().saturating_sub(1);
946+
let max_scroll = if self.story_preview {
947+
crate::ui::markdown::markdown_preview_lines(&self.story_source)
948+
.len()
949+
.saturating_sub(1)
950+
} else {
951+
self.story_source.lines().count().saturating_sub(1)
952+
};
940953
if delta > 0 {
941954
self.story_source_scroll = (self.story_source_scroll + delta as usize).min(max_scroll);
942955
} else {
@@ -1271,6 +1284,39 @@ impl App {
12711284
self.story_selected = self.story_selected.saturating_sub(1);
12721285
}
12731286

1287+
pub fn scroll_palette(&mut self, delta: i16) {
1288+
if self.overlay != Some(Overlay::Palette) {
1289+
return;
1290+
}
1291+
let len = self.commands().len();
1292+
if len == 0 {
1293+
return;
1294+
}
1295+
if delta > 0 {
1296+
self.palette_selected = (self.palette_selected + delta as usize).min(len - 1);
1297+
} else {
1298+
self.palette_selected = self
1299+
.palette_selected
1300+
.saturating_sub(delta.unsigned_abs() as usize);
1301+
}
1302+
}
1303+
1304+
pub fn scroll_settings(&mut self, delta: i16) {
1305+
let Some(form) = self.settings_form.as_mut() else {
1306+
return;
1307+
};
1308+
if self.overlay != Some(Overlay::Settings) {
1309+
return;
1310+
}
1311+
for _ in 0..delta.unsigned_abs() {
1312+
if delta > 0 {
1313+
form.focus_next();
1314+
} else {
1315+
form.focus_prev();
1316+
}
1317+
}
1318+
}
1319+
12741320
pub fn toggle_selected_story(&mut self) {
12751321
if self.overlay != Some(Overlay::GenerateStories)
12761322
|| !matches!(
@@ -1862,12 +1908,32 @@ impl App {
18621908
self.scroll_editor_description(delta);
18631909
return;
18641910
}
1911+
Some(Overlay::Notes) => {
1912+
self.scroll_editor_description(delta);
1913+
return;
1914+
}
18651915
Some(Overlay::Help) => {
18661916
self.scroll_help(delta);
18671917
return;
18681918
}
18691919
Some(Overlay::GenerateStories) => {
1870-
self.scroll_story_source(delta);
1920+
if self.story_tab() == StoryTab::Generated {
1921+
if delta > 0 {
1922+
self.move_story_selection_down();
1923+
} else {
1924+
self.move_story_selection_up();
1925+
}
1926+
} else {
1927+
self.scroll_story_source(delta);
1928+
}
1929+
return;
1930+
}
1931+
Some(Overlay::Palette) => {
1932+
self.scroll_palette(delta);
1933+
return;
1934+
}
1935+
Some(Overlay::Settings) => {
1936+
self.scroll_settings(delta);
18711937
return;
18721938
}
18731939
_ => {}

src/main.rs

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ fn run(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, app: &mut App) ->
111111
let mut config = load_config();
112112
prepare_startup(&config, app);
113113
let mut job_rx: Option<Receiver<JobResult>> = None;
114+
let mut mouse_capture = true;
114115

115116
loop {
116117
if app.config_dirty {
@@ -132,14 +133,71 @@ fn run(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, app: &mut App) ->
132133
}
133134

134135
if event::poll(Duration::from_millis(40))? {
135-
handle_event(event::read()?, app);
136+
let event = event::read()?;
137+
if mouse_capture_toggle_requested(&event) {
138+
mouse_capture = !mouse_capture;
139+
set_mouse_capture(terminal, mouse_capture)?;
140+
report_mouse_capture(app, mouse_capture);
141+
continue;
142+
}
143+
if mouse_capture && terminal_mouse_requested(&event) {
144+
mouse_capture = false;
145+
set_mouse_capture(terminal, false)?;
146+
report_mouse_capture(app, false);
147+
continue;
148+
}
149+
handle_event(event, app);
136150
}
137151
if let Some(text) = app.clipboard_request.take() {
138152
let _ = copy_to_clipboard(&text);
139153
}
140154
}
141155
}
142156

157+
fn set_mouse_capture(
158+
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
159+
enabled: bool,
160+
) -> io::Result<()> {
161+
if enabled {
162+
execute!(terminal.backend_mut(), EnableMouseCapture)?;
163+
} else {
164+
execute!(terminal.backend_mut(), DisableMouseCapture)?;
165+
}
166+
Ok(())
167+
}
168+
169+
fn mouse_capture_toggle_requested(event: &Event) -> bool {
170+
matches!(
171+
event,
172+
Event::Key(key)
173+
if (key.kind == KeyEventKind::Press || key.kind == KeyEventKind::Repeat)
174+
&& matches!(key.code, KeyCode::Char('m') | KeyCode::Char('M'))
175+
&& key.modifiers.contains(KeyModifiers::ALT)
176+
)
177+
}
178+
179+
fn terminal_mouse_requested(event: &Event) -> bool {
180+
matches!(
181+
event,
182+
Event::Mouse(mouse)
183+
if matches!(
184+
mouse.kind,
185+
MouseEventKind::Down(MouseButton::Right) | MouseEventKind::Up(MouseButton::Right)
186+
)
187+
)
188+
}
189+
190+
fn report_mouse_capture(app: &mut App, enabled: bool) {
191+
if enabled {
192+
app.set_status(StatusKind::Info, "App mouse enabled");
193+
} else {
194+
app.set_status(
195+
StatusKind::Info,
196+
"Terminal select/copy/paste enabled. Alt+M restores app mouse.",
197+
);
198+
}
199+
}
200+
143201
fn prepare_startup(config: &Result<AppConfig, ConfigError>, app: &mut App) {
144202
app.settings = lane_ratatui_premium::settings::load();
145203
match config {
@@ -827,6 +885,28 @@ mod tests {
827885
assert_eq!(app.hover, Some(ClickTarget::Help));
828886
}
829887

888+
#[test]
889+
fn alt_m_toggles_terminal_mouse_mode() {
890+
let event = Event::Key(crossterm::event::KeyEvent::new(
891+
KeyCode::Char('m'),
892+
KeyModifiers::ALT,
893+
));
894+
895+
assert!(mouse_capture_toggle_requested(&event));
896+
}
897+
898+
#[test]
899+
fn right_click_requests_terminal_mouse_mode() {
900+
let event = Event::Mouse(crossterm::event::MouseEvent {
901+
kind: MouseEventKind::Down(MouseButton::Right),
902+
column: 5,
903+
row: 5,
904+
modifiers: KeyModifiers::NONE,
905+
});
906+
907+
assert!(terminal_mouse_requested(&event));
908+
}
909+
830910
#[test]
831911
fn startup_requests_connect_when_config_is_valid() {
832912
let config = AppConfig::from_env_values(EnvConfig {

src/ui/help.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ fn help_columns() -> (Vec<Line<'static>>, Vec<Line<'static>>) {
7070
("c", "Connect"),
7171
("s / r", "Sync"),
7272
("Ctrl+C", "Copy field"),
73-
("Drag", "Text select"),
73+
("Alt+M", "Mouse mode"),
74+
("Right click", "Terminal select/paste"),
7475
("q", "Quit"),
7576
],
7677
);

0 commit comments

Comments
 (0)