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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Added
- add `window.opacity` for a transparent background (compositor-dependent)
- hold Shift while clicking or dragging to bypass an application's mouse reporting — selection and link opening now work inside full-screen apps (vim, tmux, Claude Code)

### Fixed
- honour SGR colon subparameters: `4:0` now turns underline off (was leaving the whole screen underlined) and `38:2:r:g:b` / `48:5:n` colours are applied
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Renders entirely via a CPU pixel buffer — no GPU, no OpenGL, no Vulkan.
- **Multi-tab** — independent pane trees and font metrics per tab
- **Scrollback search** — live match highlighting across 10 000-line buffer; history navigable with ↑/↓, persisted to `~/.config/mmterm/search_history`
- **Themes** — 11 built-in themes (including `ereader`, a warm parchment/sepia theme for long sessions, and `totoro`, a Studio Ghibli–inspired forest-night theme); custom themes via `~/.config/mmterm/themes/`; each named scope remembers its own theme independently of the global config
- **OSC 8 hyperlinks** — clickable URLs rendered in the terminal; hovering a link shows its full URL in the status bar
- **OSC 8 hyperlinks** — clickable URLs rendered in the terminal; hovering a link shows its full URL in the status bar; inside apps that grab the mouse (vim, tmux, Claude Code) hold `Shift` to click links and select text
- **OSC 52 clipboard sync** — copy/paste over SSH without extra tools
- **Focus reporting** — `?1004h/l` sends `\e[I`/`\e[O` on window, tab, and pane focus changes; neovim `autoread` and tmux work correctly
- **DEC line drawing** — box-drawing characters for ncurses TUI apps (`dialog`, `nmtui`, `mutt`)
Expand Down
22 changes: 20 additions & 2 deletions src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ fn palette_move_selection(selected: usize, filtered_len: usize, up: bool) -> usi
}
}

/// Whether a mouse click is reported to the application instead of being
/// handled locally. `bypass` is set when the press started with Shift held —
/// the xterm convention for reaching the terminal's own selection and links
/// while a full-screen application has mouse reporting enabled.
fn forward_click_to_pty(mouse_mode: u16, btn_code: u8, bypass: bool) -> bool {
mouse_mode >= 1000 && btn_code < 3 && !bypass
}

fn cursor_icon_for_hover(hover_sep: Option<&SeparatorHandle>, has_url: bool) -> CursorIcon {
if let Some(h) = hover_sep {
match h.dir {
Expand Down Expand Up @@ -579,7 +587,7 @@ impl App {
self.request_redraw();
}
let (mouse_mode, mouse_sgr) = self.active_mouse_mode();
if mouse_mode >= 1002 {
if mouse_mode >= 1002 && !self.state.mouse_bypass_reporting {
self.report_pty_mouse_move(px, py, mouse_mode, mouse_sgr);
} else if self.state.mouse_selecting {
self.update_mouse_selection(px, py);
Expand Down Expand Up @@ -663,8 +671,18 @@ impl App {
MouseButton::Right => 2u8,
_ => 3u8,
};
// Shift on press bypasses mouse reporting for the whole press-release
// pair, so selection and link clicks stay usable inside full-screen
// applications that grab the mouse.
if state == ElementState::Pressed && self.modifiers.state().shift_key() {
self.state.mouse_bypass_reporting = true;
}
let bypass = self.state.mouse_bypass_reporting;
if state == ElementState::Released {
self.state.mouse_bypass_reporting = false;
}
let (mouse_mode, mouse_sgr) = self.active_mouse_mode();
if mouse_mode >= 1000 && btn_code < 3 {
if forward_click_to_pty(mouse_mode, btn_code, bypass) {
self.send_pty_mouse_click(btn_code, state, button, mouse_sgr);
return;
}
Expand Down
27 changes: 27 additions & 0 deletions src/app_event_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,30 @@ fn push_search_history_clears_pending_before_buffer() {
"committing a search must clear the saved in-progress query"
);
}

#[test]
fn click_is_forwarded_when_application_enabled_mouse_reporting() {
assert!(super::forward_click_to_pty(1000, 0, false));
assert!(super::forward_click_to_pty(1002, 1, false));
assert!(super::forward_click_to_pty(1006, 2, false));
}

#[test]
fn click_is_handled_locally_without_mouse_reporting() {
assert!(!super::forward_click_to_pty(0, 0, false));
}

#[test]
fn shift_click_bypasses_mouse_reporting() {
// Shift must reach the terminal's own selection / link handling even while
// a full-screen application (Claude Code, vim, tmux) grabs the mouse.
assert!(!super::forward_click_to_pty(1000, 0, true));
assert!(!super::forward_click_to_pty(1002, 1, true));
assert!(!super::forward_click_to_pty(1006, 2, true));
}

#[test]
fn buttons_beyond_right_are_never_forwarded() {
assert!(!super::forward_click_to_pty(1000, 3, false));
assert!(!super::forward_click_to_pty(1000, 3, true));
}
5 changes: 5 additions & 0 deletions src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ pub struct AppState {
pub clipboard: Option<Clipboard>,
pub mouse_pos: Option<(f64, f64)>,
pub mouse_selecting: bool,
/// Set while a left-button press started with Shift held: mouse reporting is
/// bypassed for the whole press-drag-release so the click selects text or
/// opens a link locally instead of reaching the application.
pub mouse_bypass_reporting: bool,
/// Time, pixel position, and running click count of the last left-button
/// press, for multi-click detection (double-click word / triple-click line).
pub last_click: Option<(Instant, f64, f64, u8)>,
Expand Down Expand Up @@ -149,6 +153,7 @@ impl AppState {
clipboard: Clipboard::new().ok(),
mouse_pos: None,
mouse_selecting: false,
mouse_bypass_reporting: false,
last_click: None,
search_matches: Vec::new(),
search_current: 0,
Expand Down