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
2 changes: 1 addition & 1 deletion docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ font_size = 32.0
text_background_enabled = false
arrow_length = 20.0
arrow_angle = 30.0
arrow_head_at_end = false
arrow_head_at_end = true
show_status_bar = true
```

Expand Down
44 changes: 44 additions & 0 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,50 @@ use crate::cli::Cli;
use crate::session_override::set_runtime_session_override;
use env::env_flag_enabled;
use session::run_session_cli_commands;
use std::process::{Command, Stdio};
use usage::{log_overlay_controls, print_usage};

fn maybe_detach_active(cli: &Cli) -> anyhow::Result<bool> {
if !(cli.active || cli.freeze) {
return Ok(false);
}
if env_flag_enabled("WAYSCRIBER_NO_DETACH") || std::env::var_os("WAYSCRIBER_DETACHED").is_some()
{
return Ok(false);
}
let exe = std::env::current_exe()?;
let args: Vec<std::ffi::OsString> = std::env::args_os().skip(1).collect();
let mut cmd = Command::new(exe);
cmd.args(args)
.env("WAYSCRIBER_DETACHED", "1")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
cmd.spawn()?;
Ok(true)
}

#[cfg(unix)]
fn detach_from_tty() {
// Start a new session to drop the controlling terminal (prevents stuck shells).
unsafe {
let _ = libc::setsid();
}
// Best-effort close of stdio if they still point to a TTY.
for fd in [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO] {
let is_tty = unsafe { libc::isatty(fd) } == 1;
if is_tty {
let _ = unsafe { libc::close(fd) };
}
}
}

pub fn run(cli: Cli) -> anyhow::Result<()> {
#[cfg(unix)]
if std::env::var_os("WAYSCRIBER_DETACHED").is_some() {
detach_from_tty();
}

let session_override = if cli.resume_session {
Some(true)
} else if cli.no_resume_session {
Expand Down Expand Up @@ -45,6 +86,9 @@ pub fn run(cli: Cli) -> anyhow::Result<()> {
let mut daemon = crate::daemon::Daemon::new(cli.mode, !tray_disabled, session_override);
daemon.run()?;
} else if cli.active || cli.freeze {
if maybe_detach_active(&cli)? {
return Ok(());
}
// One-shot mode: show overlay immediately and exit when done
log_overlay_controls(cli.freeze);

Expand Down
3 changes: 1 addition & 2 deletions src/backend/wayland/backend/event_loop/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ pub(super) fn run_event_loop(
let capture_active = state.capture.is_in_progress()
|| state.frozen.is_in_progress()
|| state.zoom.is_in_progress()
|| state.overlay_suppressed();
|| state.overlay_blocks_event_loop();
let frame_callback_pending = state.surface.frame_callback_pending();
let vsync_enabled = state.config.performance.enable_vsync;

Expand Down Expand Up @@ -103,7 +103,6 @@ pub(super) fn run_event_loop(
} else {
min_timeout(animation_timeout, autosave_timeout)
};

if let Err(e) = dispatch::dispatch_events(event_queue, state, capture_active, timeout) {
warn!("Event queue error: {}", e);
loop_error = Some(e);
Expand Down
40 changes: 40 additions & 0 deletions src/backend/wayland/handlers/pointer/axis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use log::debug;
use smithay_client_toolkit::seat::pointer::{AxisScroll, PointerEvent};

use crate::input::Tool;
use crate::input::state::COMMAND_PALETTE_MAX_VISIBLE;

use super::*;

Expand All @@ -19,6 +20,45 @@ impl WaylandState {
} else {
0
};
// Handle command palette scrolling
if self.input_state.command_palette_open {
if scroll_direction != 0 {
let filtered_count = self.input_state.filtered_commands().len();
let max_scroll = filtered_count.saturating_sub(COMMAND_PALETTE_MAX_VISIBLE);

if scroll_direction > 0 {
// Scroll down
if self.input_state.command_palette_scroll < max_scroll {
self.input_state.command_palette_scroll += 1;
// Also move selection if it's above the visible area
if self.input_state.command_palette_selected
< self.input_state.command_palette_scroll
{
self.input_state.command_palette_selected =
self.input_state.command_palette_scroll;
}
self.input_state.needs_redraw = true;
}
} else {
// Scroll up
if self.input_state.command_palette_scroll > 0 {
self.input_state.command_palette_scroll -= 1;
// Also move selection if it's below the visible area
if self.input_state.command_palette_selected
>= self.input_state.command_palette_scroll + COMMAND_PALETTE_MAX_VISIBLE
{
self.input_state.command_palette_selected =
self.input_state.command_palette_scroll
+ COMMAND_PALETTE_MAX_VISIBLE
- 1;
}
self.input_state.needs_redraw = true;
}
}
}
return;
}

if self.input_state.show_help {
if scroll_direction != 0 {
let delta = if scroll_direction > 0 { 1.0 } else { -1.0 };
Expand Down
197 changes: 187 additions & 10 deletions src/backend/wayland/handlers/pointer/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,199 @@ use smithay_client_toolkit::seat::pointer::CursorIcon;
use wayland_client::Connection;

use super::*;
use crate::backend::wayland::toolbar::ToolbarCursorHint;
use crate::input::{
BoardPickerCursorHint, ColorPickerCursorHint, CommandPaletteCursorHint, ContextMenuCursorHint,
DrawingState, HelpOverlayCursorHint, SelectionHandle,
};

impl WaylandState {
pub(super) fn update_pointer_cursor(&mut self, toolbar_hover: bool, conn: &Connection) {
if let Some(pointer) = self.themed_pointer.as_ref() {
let icon = if toolbar_hover {
CursorIcon::Default
let icon = self.compute_cursor_icon(toolbar_hover);
if let Some(pointer) = self.themed_pointer.as_ref()
&& self.current_pointer_shape != Some(icon)
{
if let Err(err) = pointer.set_cursor(conn, icon) {
warn!("Failed to set cursor icon: {}", err);
} else {
CursorIcon::Crosshair
};
if self.current_pointer_shape != Some(icon) {
if let Err(err) = pointer.set_cursor(conn, icon) {
warn!("Failed to set cursor icon: {}", err);
} else {
self.current_pointer_shape = Some(icon);
self.current_pointer_shape = Some(icon);
}
}
}

/// Computes the appropriate cursor icon based on current context.
fn compute_cursor_icon(&mut self, toolbar_hover: bool) -> CursorIcon {
// Check color picker popup first (takes priority)
if self.input_state.is_color_picker_popup_open() {
let (mx, my) = self.current_mouse();
if let Some(layout) = self.input_state.color_picker_popup_layout() {
// When dragging on gradient, always show crosshair
if self.input_state.color_picker_popup_is_dragging() {
return CursorIcon::Crosshair;
}
return match layout.cursor_hint_at(mx as f64, my as f64) {
ColorPickerCursorHint::Text => CursorIcon::Text,
ColorPickerCursorHint::Crosshair => CursorIcon::Crosshair,
ColorPickerCursorHint::Pointer => CursorIcon::Pointer,
ColorPickerCursorHint::Default => CursorIcon::Default,
};
}
}

// Check board picker popup
if self.input_state.is_board_picker_open() {
let (mx, my) = self.current_mouse();
if let Some(hint) = self.input_state.board_picker_cursor_hint_at(mx, my) {
return match hint {
BoardPickerCursorHint::Text => CursorIcon::Text,
BoardPickerCursorHint::Pointer => CursorIcon::Pointer,
BoardPickerCursorHint::Grab => CursorIcon::Grab,
BoardPickerCursorHint::Grabbing => CursorIcon::Grabbing,
BoardPickerCursorHint::Default => CursorIcon::Default,
};
}
}

// Check context menu
if self.input_state.is_context_menu_open() {
let (mx, my) = self.current_mouse();
if let Some(hint) = self.input_state.context_menu_cursor_hint_at(mx, my) {
return match hint {
ContextMenuCursorHint::Pointer => CursorIcon::Pointer,
ContextMenuCursorHint::Default => CursorIcon::Default,
};
}
}

// Check command palette
if self.input_state.command_palette_open {
let (mx, my) = self.current_mouse();
let screen_width = self.surface.width();
let screen_height = self.surface.height();
if let Some(hint) =
self.input_state
.command_palette_cursor_hint_at(mx, my, screen_width, screen_height)
{
return match hint {
CommandPaletteCursorHint::Text => CursorIcon::Text,
CommandPaletteCursorHint::Pointer => CursorIcon::Pointer,
CommandPaletteCursorHint::Default => CursorIcon::Default,
};
}
}

// Check help overlay
if self.input_state.show_help {
let (mx, my) = self.current_mouse();
let screen_width = self.surface.width();
let screen_height = self.surface.height();
if let Some(hint) =
self.input_state
.help_overlay_cursor_hint_at(mx, my, screen_width, screen_height)
{
return match hint {
HelpOverlayCursorHint::Text => CursorIcon::Text,
HelpOverlayCursorHint::Default => CursorIcon::Default,
};
}
}

// Inline toolbar cursor hints (when using inline mode)
if self.inline_toolbars_active()
&& self.pointer_over_toolbar()
&& let Some(hint) = self.inline_toolbar_cursor_hint()
{
return match hint {
ToolbarCursorHint::Pointer => CursorIcon::Pointer,
ToolbarCursorHint::Grab => CursorIcon::Grab,
ToolbarCursorHint::Crosshair => CursorIcon::Crosshair,
ToolbarCursorHint::Default => CursorIcon::Default,
};
}

// Layer-shell toolbar cursor hints (sliders get grab, buttons get pointer, etc.)
if toolbar_hover {
if let Some(hint) = self.toolbar.cursor_hint() {
return match hint {
ToolbarCursorHint::Pointer => CursorIcon::Pointer,
ToolbarCursorHint::Grab => CursorIcon::Grab,
ToolbarCursorHint::Crosshair => CursorIcon::Crosshair,
ToolbarCursorHint::Default => CursorIcon::Default,
};
}
return CursorIcon::Default;
}

// Check drawing state for context
match &self.input_state.state {
// Text input mode - show text cursor
DrawingState::TextInput { .. } => {
return CursorIcon::Text;
}
// Dragging selection - show grabbing cursor
DrawingState::MovingSelection { .. } => {
return CursorIcon::Grabbing;
}
// Resizing text - show resize cursor
DrawingState::ResizingText { .. } => {
return CursorIcon::SeResize;
}
// Drawing - use crosshair
DrawingState::Drawing { .. } => {
return CursorIcon::Crosshair;
}
// Selecting (marquee) - use crosshair
DrawingState::Selecting { .. } => {
return CursorIcon::Crosshair;
}
// Pending text click - use default
DrawingState::PendingTextClick { .. } => {
return CursorIcon::Default;
}
// Resizing selection - show appropriate resize cursor
DrawingState::ResizingSelection { handle, .. } => {
return match handle {
SelectionHandle::TopLeft | SelectionHandle::BottomRight => {
CursorIcon::NwseResize
}
SelectionHandle::TopRight | SelectionHandle::BottomLeft => {
CursorIcon::NeswResize
}
SelectionHandle::Top | SelectionHandle::Bottom => CursorIcon::NsResize,
SelectionHandle::Left | SelectionHandle::Right => CursorIcon::EwResize,
};
}
// Idle - check for hover contexts
DrawingState::Idle => {}
}

// Check if hovering over selection handles
let (mx, my) = self.current_mouse();
if let Some(handle) = self.input_state.hit_selection_handle(mx, my) {
return match handle {
SelectionHandle::TopLeft | SelectionHandle::BottomRight => CursorIcon::NwseResize,
SelectionHandle::TopRight | SelectionHandle::BottomLeft => CursorIcon::NeswResize,
SelectionHandle::Top | SelectionHandle::Bottom => CursorIcon::NsResize,
SelectionHandle::Left | SelectionHandle::Right => CursorIcon::EwResize,
};
}

// Check if hovering over text resize handle
if self.input_state.hit_text_resize_handle(mx, my).is_some() {
return CursorIcon::SeResize;
}

// Check if hovering over a selected shape (for move)
if let Some(hit_id) = self.input_state.hit_test_at(mx, my)
&& self
.input_state
.selected_shape_ids_set()
.is_some_and(|set| set.contains(&hit_id))
{
return CursorIcon::Grab;
}

// Default: crosshair for drawing
CursorIcon::Crosshair
}
}
21 changes: 19 additions & 2 deletions src/backend/wayland/handlers/pointer/press.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,25 @@ impl WaylandState {
inline_active: bool,
button: u32,
) {
// Block pointer input when modal overlays are active
if self.input_state.command_palette_open || self.input_state.tour_active {
// Block pointer input when tour is active
if self.input_state.tour_active {
return;
}

// Handle command palette clicks
if self.input_state.command_palette_open {
if button == BTN_LEFT {
let screen_width = self.surface.width();
let screen_height = self.surface.height();
if self.input_state.handle_command_palette_click(
event.position.0 as i32,
event.position.1 as i32,
screen_width,
screen_height,
) {
self.set_suppress_next_release(true);
}
}
return;
}

Expand Down
6 changes: 6 additions & 0 deletions src/backend/wayland/handlers/pointer/release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@ impl WaylandState {
inline_active: bool,
button: u32,
) {
// Swallow releases after modal clicks (e.g., palette dismiss)
if self.take_suppress_next_release() {
return;
}

// Block pointer input when modal overlays are active
if self.input_state.command_palette_open || self.input_state.tour_active {
// For command palette, press handles the click - release is a no-op
return;
}

Expand Down
Loading