hide mouse after idle time based on `mouse-hide-after` config
parent
1d7fe9e70d
commit
de79e79c82
|
|
@ -540,6 +540,14 @@ extension Ghostty {
|
|||
return v;
|
||||
}
|
||||
|
||||
var mouseHideAfter: UInt {
|
||||
guard let config = self.config else { return 0 }
|
||||
var v: UInt = 0
|
||||
let key = "mouse-hide-after"
|
||||
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
|
||||
return v;
|
||||
}
|
||||
|
||||
var undoTimeout: Duration {
|
||||
guard let config = self.config else { return .seconds(5) }
|
||||
var v: UInt = 0
|
||||
|
|
|
|||
|
|
@ -130,6 +130,10 @@ extension Ghostty {
|
|||
// then the view is moved to a new window.
|
||||
var initialSize: NSSize? = nil
|
||||
|
||||
// Timer used for `mouse-hide-after` behavior on macOS. This hides the mouse
|
||||
// cursor after a period of no mouse movement.
|
||||
private var mouseHideAfterTimer: Timer? = nil
|
||||
|
||||
// A content size received through sizeDidChange that may in some cases
|
||||
// be different from the frame size.
|
||||
private var contentSizeBacking: NSSize?
|
||||
|
|
@ -775,6 +779,30 @@ extension Ghostty {
|
|||
userInfo: nil))
|
||||
}
|
||||
|
||||
/// Reset the `mouse-hide-after` timer on mouse movement, if configured.
|
||||
private func resetMouseHideAfterTimer() {
|
||||
mouseHideAfterTimer?.invalidate()
|
||||
mouseHideAfterTimer = nil
|
||||
|
||||
// Note: DerivedConfig does not expose the full Ghostty.Config, so we
|
||||
// read directly from the global Ghostty config instead.
|
||||
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return }
|
||||
let ms = appDelegate.ghostty.config.mouseHideAfter
|
||||
guard ms > 0 else { return }
|
||||
|
||||
let interval = TimeInterval(ms) / 1000.0
|
||||
mouseHideAfterTimer = Timer.scheduledTimer(
|
||||
withTimeInterval: interval,
|
||||
repeats: false,
|
||||
block: { [weak self] _ in
|
||||
guard let self else { return }
|
||||
// Hide the cursor. This will remain hidden until the mouse
|
||||
// moves again (or other platform behavior such as new window).
|
||||
NSCursor.setHiddenUntilMouseMoves(true)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override func viewDidChangeBackingProperties() {
|
||||
super.viewDidChangeBackingProperties()
|
||||
|
||||
|
|
@ -898,6 +926,8 @@ extension Ghostty {
|
|||
mods: .init(nsFlags: event.modifierFlags)
|
||||
)
|
||||
surfaceModel.sendMousePos(mouseEvent)
|
||||
|
||||
resetMouseHideAfterTimer()
|
||||
}
|
||||
|
||||
override func mouseExited(with event: NSEvent) {
|
||||
|
|
@ -931,6 +961,8 @@ extension Ghostty {
|
|||
)
|
||||
surfaceModel.sendMousePos(mouseEvent)
|
||||
|
||||
resetMouseHideAfterTimer()
|
||||
|
||||
// Handle focus-follows-mouse
|
||||
if let window,
|
||||
let controller = window.windowController as? BaseTerminalController,
|
||||
|
|
|
|||
|
|
@ -243,6 +243,9 @@ const Mouse = struct {
|
|||
/// True if the mouse is hidden
|
||||
hidden: bool = false,
|
||||
|
||||
/// The last time we observed mouse movement
|
||||
last_move_time: ?std.time.Instant = null,
|
||||
|
||||
/// True if the mouse position is currently over a link.
|
||||
over_link: bool = false,
|
||||
|
||||
|
|
@ -298,6 +301,7 @@ const DerivedConfig = struct {
|
|||
font: font.SharedGridSet.DerivedConfig,
|
||||
mouse_interval: u64,
|
||||
mouse_hide_while_typing: bool,
|
||||
mouse_hide_after: Duration,
|
||||
mouse_reporting: bool,
|
||||
mouse_scroll_multiplier: configpkg.MouseScrollMultiplier,
|
||||
mouse_shift_capture: configpkg.MouseShiftCapture,
|
||||
|
|
@ -373,6 +377,7 @@ const DerivedConfig = struct {
|
|||
.font = try font.SharedGridSet.DerivedConfig.init(alloc, config),
|
||||
.mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms
|
||||
.mouse_hide_while_typing = config.@"mouse-hide-while-typing",
|
||||
.mouse_hide_after = config.@"mouse-hide-after",
|
||||
.mouse_reporting = config.@"mouse-reporting",
|
||||
.mouse_scroll_multiplier = config.@"mouse-scroll-multiplier",
|
||||
.mouse_shift_capture = config.@"mouse-shift-capture",
|
||||
|
|
@ -1537,6 +1542,23 @@ fn modsChanged(self: *Surface, mods: input.Mods) void {
|
|||
}
|
||||
}
|
||||
|
||||
/// Hide the mouse if `mouse-hide-after` is configured and the mouse has been
|
||||
/// idle (no movement) for at least that duration.
|
||||
pub fn maybeHideMouseAfterIdle(self: *Surface) void {
|
||||
if (self.config.mouse_hide_after.duration == 0 or self.mouse.hidden) return;
|
||||
|
||||
const now = std.time.Instant.now() catch {
|
||||
return;
|
||||
};
|
||||
|
||||
const last = self.mouse.last_move_time orelse return;
|
||||
|
||||
const since = now.since(last);
|
||||
if (since >= self.config.mouse_hide_after.duration) {
|
||||
self.hideMouse();
|
||||
}
|
||||
}
|
||||
|
||||
/// Call this whenever the mouse moves or mods changed. The time
|
||||
/// at which this is called may matter for the correctness of other
|
||||
/// mouse events (see cursorPosCallback) but this is shared logic
|
||||
|
|
@ -2559,6 +2581,10 @@ pub fn keyCallback(
|
|||
crash.sentry.thread_state = self.crashThreadState();
|
||||
defer crash.sentry.thread_state = null;
|
||||
|
||||
// If the mouse has been idle long enough, hide it. Typing does not reset
|
||||
// the idle timer; we only track mouse movement for this.
|
||||
self.maybeHideMouseAfterIdle();
|
||||
|
||||
// Setup our inspector event if we have an inspector.
|
||||
var insp_ev: ?inspectorpkg.key.Event = if (self.inspector != null) ev: {
|
||||
var copy = event;
|
||||
|
|
@ -4373,6 +4399,12 @@ pub fn cursorPosCallback(
|
|||
|
||||
// log.debug("cursor pos x={} y={} mods={?}", .{ pos.x, pos.y, mods });
|
||||
|
||||
// Any cursor position update is considered mouse movement and resets the
|
||||
// idle timer used by `mouse-hide-after`.
|
||||
if (std.time.Instant.now()) |now| {
|
||||
self.mouse.last_move_time = now;
|
||||
} else |_| {}
|
||||
|
||||
// If the position is negative, it is outside our viewport and
|
||||
// we need to clear any hover states.
|
||||
if (pos.x < 0 or pos.y < 0) {
|
||||
|
|
|
|||
|
|
@ -587,6 +587,11 @@ pub const Surface = extern struct {
|
|||
// Progress bar
|
||||
progress_bar_timer: ?c_uint = null,
|
||||
|
||||
/// Timer source id for `mouse-hide-after` idle hiding. When non-null,
|
||||
/// a one-shot GLib timeout is scheduled to hide the mouse after the
|
||||
/// configured idle duration.
|
||||
mouse_hide_after_timer: ?c_uint = null,
|
||||
|
||||
// True while the bell is ringing. This will be set to false (after
|
||||
// true) under various scenarios, but can also manually be set to
|
||||
// false by a parent widget.
|
||||
|
|
@ -911,6 +916,23 @@ pub const Surface = extern struct {
|
|||
return @intFromBool(glib.SOURCE_REMOVE);
|
||||
}
|
||||
|
||||
/// Timer callback for `mouse-hide-after`. Hides the mouse after a period
|
||||
/// of no mouse movement by delegating to the core surface.
|
||||
fn mouseHideAfterTimer(ud: ?*anyopaque) callconv(.c) c_int {
|
||||
const self: *Self = @ptrCast(@alignCast(ud orelse
|
||||
return @intFromBool(glib.SOURCE_REMOVE)));
|
||||
const priv = self.private();
|
||||
|
||||
// Clear our timer handle first to avoid reusing it.
|
||||
priv.mouse_hide_after_timer = null;
|
||||
|
||||
if (priv.core_surface) |surface| {
|
||||
surface.maybeHideMouseAfterIdle();
|
||||
}
|
||||
|
||||
return @intFromBool(glib.SOURCE_REMOVE);
|
||||
}
|
||||
|
||||
/// Request that this terminal come to the front and become focused.
|
||||
/// It is up to the embedding widget to react to this.
|
||||
pub fn present(self: *Self) void {
|
||||
|
|
@ -1722,6 +1744,13 @@ pub const Surface = extern struct {
|
|||
priv.progress_bar_timer = null;
|
||||
}
|
||||
|
||||
if (priv.mouse_hide_after_timer) |timer| {
|
||||
if (glib.Source.remove(timer) == 0) {
|
||||
log.warn("unable to remove mouse hide-after timer", .{});
|
||||
}
|
||||
priv.mouse_hide_after_timer = null;
|
||||
}
|
||||
|
||||
if (priv.idle_rechild) |v| {
|
||||
if (glib.Source.remove(v) == 0) {
|
||||
log.warn("unable to remove idle source", .{});
|
||||
|
|
@ -2660,6 +2689,25 @@ pub const Surface = extern struct {
|
|||
// Our pos changed, update
|
||||
priv.cursor_pos = pos;
|
||||
|
||||
// Reset `mouse-hide-after` idle timer if configured.
|
||||
if (priv.mouse_hide_after_timer) |timer| {
|
||||
if (glib.Source.remove(timer) == 0) {
|
||||
log.warn("unable to remove mouse hide-after timer", .{});
|
||||
}
|
||||
priv.mouse_hide_after_timer = null;
|
||||
}
|
||||
if (priv.config) |config_obj| {
|
||||
const cfg = config_obj.get();
|
||||
const ms = cfg.@"mouse-hide-after".asMilliseconds();
|
||||
if (ms > 0) {
|
||||
priv.mouse_hide_after_timer = glib.timeoutAdd(
|
||||
ms,
|
||||
mouseHideAfterTimer,
|
||||
self,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify the callback
|
||||
if (priv.core_surface) |surface| {
|
||||
const gtk_mods = event.getModifierState();
|
||||
|
|
|
|||
|
|
@ -820,6 +820,22 @@ palette: Palette = .{},
|
|||
/// the mouse is shown again when a new window, tab, or split is created.
|
||||
@"mouse-hide-while-typing": bool = false,
|
||||
|
||||
/// Hide the mouse after a period of mouse inactivity (no mouse movement).
|
||||
/// When set, the mouse is hidden once it has been idle for at least this long
|
||||
/// and becomes visible again when the mouse is used (button, movement, etc.).
|
||||
/// Typing does not affect the idle timer for this configuration; once the
|
||||
/// mouse has been idle long enough to hide, it will remain hidden while
|
||||
/// typing until the mouse is moved again.
|
||||
///
|
||||
/// Set this to a duration such as `5s` to hide the mouse 5 seconds after the
|
||||
/// last mouse movement. The default value of `0` disables this behavior.
|
||||
///
|
||||
/// This is not mutually exclusive with `mouse-hide-while-typing`; both can be
|
||||
/// enabled at the same time.
|
||||
///
|
||||
/// Available since 1.3.0
|
||||
@"mouse-hide-after": Duration = .{ .duration = 0 },
|
||||
|
||||
/// When to scroll the surface to the bottom. The format of this is a list of
|
||||
/// options to enable separated by commas. If you prefix an option with `no-`
|
||||
/// then it is disabled. If you omit an option, its default value is used.
|
||||
|
|
|
|||
Loading…
Reference in New Issue