Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ sha2 = "0.11.0"
thiserror = "2.0.18"
ureq = { version = "3.3.0", features = ["json", "platform-verifier"] }
url = "2.5.8"
pulldown-cmark = { version = "0.13.4", default-features = false }
72 changes: 69 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,7 @@ impl App {

self.note_input.clear();
self.notes.clear();
self.editor_description_scroll = 0;
self.notes_session_id = self.notes_session_id.wrapping_add(1);
self.note_submit_pending = None;
self.notes_issue_iid = Some(iid);
Expand Down Expand Up @@ -865,7 +866,7 @@ impl App {
pub fn scroll_editor_description(&mut self, delta: i16) {
if !matches!(
self.overlay,
Some(Overlay::Details | Overlay::EditIssue | Overlay::NewIssue)
Some(Overlay::Details | Overlay::EditIssue | Overlay::NewIssue | Overlay::Notes)
) {
return;
}
Expand All @@ -881,6 +882,12 @@ impl App {
}

fn editor_description_scroll_max(&self) -> usize {
if self.overlay == Some(Overlay::Notes) {
let notes_lines = self.notes.len().clamp(1, 5);
let input_lines = self.note_input.lines().count().max(1);
return (notes_lines + input_lines + 6).saturating_sub(1);
}

// The read-only details view always renders the markdown preview.
let preview = self.editor_preview || self.overlay == Some(Overlay::Details);
crate::ui::markdown::description_lines(
Expand Down Expand Up @@ -936,7 +943,13 @@ impl App {
if self.overlay != Some(Overlay::GenerateStories) {
return;
}
let max_scroll = self.story_source.lines().count().saturating_sub(1);
let max_scroll = if self.story_preview {
crate::ui::markdown::markdown_preview_lines(&self.story_source)
.len()
.saturating_sub(1)
} else {
self.story_source.lines().count().saturating_sub(1)
};
if delta > 0 {
self.story_source_scroll = (self.story_source_scroll + delta as usize).min(max_scroll);
} else {
Expand Down Expand Up @@ -1271,6 +1284,39 @@ impl App {
self.story_selected = self.story_selected.saturating_sub(1);
}

pub fn scroll_palette(&mut self, delta: i16) {
if self.overlay != Some(Overlay::Palette) {
return;
}
let len = self.commands().len();
if len == 0 {
return;
}
if delta > 0 {
self.palette_selected = (self.palette_selected + delta as usize).min(len - 1);
} else {
self.palette_selected = self
.palette_selected
.saturating_sub(delta.unsigned_abs() as usize);
}
}

pub fn scroll_settings(&mut self, delta: i16) {
let Some(form) = self.settings_form.as_mut() else {
return;
};
if self.overlay != Some(Overlay::Settings) {
return;
}
for _ in 0..delta.unsigned_abs() {
if delta > 0 {
form.focus_next();
} else {
form.focus_prev();
}
}
}

pub fn toggle_selected_story(&mut self) {
if self.overlay != Some(Overlay::GenerateStories)
|| !matches!(
Expand Down Expand Up @@ -1862,12 +1908,32 @@ impl App {
self.scroll_editor_description(delta);
return;
}
Some(Overlay::Notes) => {
self.scroll_editor_description(delta);
return;
}
Some(Overlay::Help) => {
self.scroll_help(delta);
return;
}
Some(Overlay::GenerateStories) => {
self.scroll_story_source(delta);
if self.story_tab() == StoryTab::Generated {
if delta > 0 {
self.move_story_selection_down();
} else {
self.move_story_selection_up();
}
} else {
self.scroll_story_source(delta);
}
return;
}
Some(Overlay::Palette) => {
self.scroll_palette(delta);
return;
}
Some(Overlay::Settings) => {
self.scroll_settings(delta);
return;
}
_ => {}
Expand Down
82 changes: 81 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,11 @@
out
}

fn run(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, app: &mut App) -> io::Result<()> {

Check failure on line 110 in src/main.rs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 19 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=RandomCodeSpace_lane&issues=AZ8ty70EyU4aPEVUBR3b&open=AZ8ty70EyU4aPEVUBR3b&pullRequest=11
let mut config = load_config();
prepare_startup(&config, app);
let mut job_rx: Option<Receiver<JobResult>> = None;
let mut mouse_capture = true;

loop {
if app.config_dirty {
Expand All @@ -132,14 +133,71 @@
}

if event::poll(Duration::from_millis(40))? {
handle_event(event::read()?, app);
let event = event::read()?;
if mouse_capture_toggle_requested(&event) {
mouse_capture = !mouse_capture;
set_mouse_capture(terminal, mouse_capture)?;
report_mouse_capture(app, mouse_capture);
continue;
}
if mouse_capture && terminal_mouse_requested(&event) {
mouse_capture = false;
set_mouse_capture(terminal, false)?;
report_mouse_capture(app, false);
continue;
}
handle_event(event, app);
}
if let Some(text) = app.clipboard_request.take() {
let _ = copy_to_clipboard(&text);
}
}
}

fn set_mouse_capture(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
enabled: bool,
) -> io::Result<()> {
if enabled {
execute!(terminal.backend_mut(), EnableMouseCapture)?;
} else {
execute!(terminal.backend_mut(), DisableMouseCapture)?;
}
Ok(())
}

fn mouse_capture_toggle_requested(event: &Event) -> bool {
matches!(
event,
Event::Key(key)
if (key.kind == KeyEventKind::Press || key.kind == KeyEventKind::Repeat)
&& matches!(key.code, KeyCode::Char('m') | KeyCode::Char('M'))
&& key.modifiers.contains(KeyModifiers::ALT)
)
}

fn terminal_mouse_requested(event: &Event) -> bool {
matches!(
event,
Event::Mouse(mouse)
if matches!(
mouse.kind,
MouseEventKind::Down(MouseButton::Right) | MouseEventKind::Up(MouseButton::Right)
)
)
}

fn report_mouse_capture(app: &mut App, enabled: bool) {
if enabled {
app.set_status(StatusKind::Info, "App mouse enabled");
} else {
app.set_status(
StatusKind::Info,
"Terminal select/copy/paste enabled. Alt+M restores app mouse.",
);
}
}

fn prepare_startup(config: &Result<AppConfig, ConfigError>, app: &mut App) {
app.settings = lane_ratatui_premium::settings::load();
match config {
Expand Down Expand Up @@ -827,6 +885,28 @@
assert_eq!(app.hover, Some(ClickTarget::Help));
}

#[test]
fn alt_m_toggles_terminal_mouse_mode() {
let event = Event::Key(crossterm::event::KeyEvent::new(
KeyCode::Char('m'),
KeyModifiers::ALT,
));

assert!(mouse_capture_toggle_requested(&event));
}

#[test]
fn right_click_requests_terminal_mouse_mode() {
let event = Event::Mouse(crossterm::event::MouseEvent {
kind: MouseEventKind::Down(MouseButton::Right),
column: 5,
row: 5,
modifiers: KeyModifiers::NONE,
});

assert!(terminal_mouse_requested(&event));
}

#[test]
fn startup_requests_connect_when_config_is_valid() {
let config = AppConfig::from_env_values(EnvConfig {
Expand Down
3 changes: 2 additions & 1 deletion src/ui/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ fn help_columns() -> (Vec<Line<'static>>, Vec<Line<'static>>) {
("c", "Connect"),
("s / r", "Sync"),
("Ctrl+C", "Copy field"),
("Drag", "Text select"),
("Alt+M", "Mouse mode"),
("Right click", "Terminal select/paste"),
("q", "Quit"),
],
);
Expand Down
Loading