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
43 changes: 40 additions & 3 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ gpui-component = "0.5.0"
gpui-component-assets = "0.5.0"
rust-embed = "8"
anyhow = "1"
thiserror = "2"
clap = { version = "4", features = ["derive"] }
freedesktop-desktop-entry = "0.8"
freedesktop-icons = "0.4"
Expand Down Expand Up @@ -42,6 +43,7 @@ toml = "0.9"
pulldown-cmark = "0.13"
libc = "0.2"
zbus = { version = "5", default-features = false, features = ["blocking-api"] }
notify = "8"

[patch.crates-io]
gpui = { git = "https://github.com/zed-industries/zed", rev = "7c724c0f1049e610c541c2f4f6a8739f91865e02" }
Expand Down
70 changes: 57 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,38 @@ zlaunch quit # Stop daemon
zlaunch reload # Restart daemon (useful after config updates)
```

### Modes

The launcher supports different modes that determine what content is shown. By default, the launcher opens in **combined** mode, showing all enabled modules together. You can also open specific modes directly or configure multiple modes to cycle through.

#### Opening specific modes

Use the `--modes` flag to open directly into specific mode(s):

```bash
# Open directly in emoji picker
zlaunch show --modes emojis

# Open directly in clipboard history
zlaunch toggle --modes clipboard

# Open with multiple modes (use Ctrl+Tab to switch)
zlaunch show --modes combined,emojis,clipboard
```

Available modes: `combined`, `applications`, `windows`, `emojis`, `clipboard`, `actions`, `search`, `calculator`, `ai`, `themes`

Mode aliases are supported: `apps`, `app`, `emoji`, `calc`, `action`, `theme`, `window`

#### Cycling between modes

When multiple modes are configured, use keyboard shortcuts to switch:

- `Ctrl+Tab` — Next mode
- `Ctrl+Shift+Tab` — Previous mode

Configure default modes in `config.toml` (see Configuration section).

### Theme management

Use the built-in theme selector in the UI, or via CLI:
Expand All @@ -122,12 +154,14 @@ zlaunch theme set NAME # Set theme by name

## Keybindings

| Key | Action |
| ------------------- | --------------------- |
| `↑` / `↓` | Navigate items |
| `Tab` / `Shift+Tab` | Navigate grid |
| `Enter` | Execute selected item |
| `Escape` | Back / Hide launcher |
| Key | Action |
| ------------------------ | --------------------- |
| `↑` / `↓` | Navigate items |
| `Tab` / `Shift+Tab` | Navigate grid |
| `Ctrl+Tab` | Next mode |
| `Ctrl+Shift+Tab` | Previous mode |
| `Enter` | Execute selected item |
| `Escape` | Back / Hide launcher |

## Configuration

Expand All @@ -148,7 +182,11 @@ window_height = 500.0
hyprland_auto_blur = false
enable_transparency = false

disabled_modules = ["clipboard", "ai"]
# Modes to cycle through with Ctrl+Tab (optional)
default_modes = ["combined", "emojis", "clipboard"]

# Modules to show in combined view, in display order (optional)
combined_modules = ["calculator", "windows", "applications", "emojis", "clipboard", "actions", "themes", "ai", "search"]

[[search_providers]]
name = "Brave"
Expand All @@ -168,19 +206,25 @@ icon = "youtube-logo"
- `window_width` / `window_height` — Launcher window size
- `enable_transparency` — Defaults to `true`. Set to `false` to force an opaque background
- `hyprland_auto_blur` — Defaults to `true`. Attempts to apply Hyprland blur rules (WIP)
- `disabled_modules` — List of modules to hide entirely
- `default_modes` — List of modes to cycle through with Ctrl+Tab. Defaults to `["combined"]`
- `combined_modules` — Ordered list of modules to include in combined view. Omit to show all modules
- `search_providers` — Custom web search providers

#### Available modules

- `actions`
- `ai`
- `calculator`
- `clipboard`
- `windows`
- `applications`
- `emojis`
- `search`
- `clipboard`
- `actions`
- `themes`
- `windows`
- `ai`
- `search`

The order in `combined_modules` determines the display order of sections in combined view.

> **Note:** The `disabled_modules` option is deprecated. Use `combined_modules` instead to specify which modules to include (and in what order).

### Search providers

Expand Down
10 changes: 9 additions & 1 deletion src/app/events.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
//! Event types for daemon communication.

use crate::config::LauncherMode;
use crate::error::IpcError;
use crate::items::ApplicationItem;
use tokio::sync::oneshot;

/// Response type for IPC operations.
pub type IpcResponse = Result<(), String>;
pub type IpcResponse = Result<(), IpcError>;

/// Events that the UI can send to the daemon.
#[derive(Debug, Clone, Copy)]
Expand All @@ -19,6 +22,7 @@ pub enum DaemonEvent {

/// Show the launcher window
Show {
modes: Option<Vec<LauncherMode>>,
response_tx: oneshot::Sender<IpcResponse>,
},

Expand All @@ -29,6 +33,7 @@ pub enum DaemonEvent {

/// Toggle the launcher window visibility
Toggle {
modes: Option<Vec<LauncherMode>>,
response_tx: oneshot::Sender<IpcResponse>,
},

Expand All @@ -47,6 +52,9 @@ pub enum DaemonEvent {
Reload {
response_tx: oneshot::Sender<IpcResponse>,
},

/// Applications have been updated (from file watcher)
ApplicationsChanged { applications: Vec<ApplicationItem> },
}

impl From<WindowEvent> for DaemonEvent {
Expand Down
17 changes: 10 additions & 7 deletions src/app/window.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::app::{DaemonEvent, DaemonEventSender, WindowEvent};
use crate::compositor::Compositor;
use crate::config::{ConfigModule, config};
use crate::config::{ConfigModule, LauncherMode, get_combined_modules};
use crate::items::{ApplicationItem, ListItem, WindowItem};
use crate::ui::LauncherView;
use gpui::{
Expand All @@ -22,23 +22,25 @@ pub struct LauncherWindow {
pub fn create_and_show_window(
applications: Vec<ApplicationItem>,
compositor: Arc<dyn Compositor>,
modes: Vec<LauncherMode>,
event_tx: DaemonEventSender,
cx: &mut App,
) -> anyhow::Result<LauncherWindow> {
// Fetch open windows from compositor (if not disabled)
let disabled_modules = config().disabled_modules.unwrap_or_default();
let windows = if disabled_modules.contains(&ConfigModule::Windows) {
Vec::new()
} else {
let combined_modules = get_combined_modules();
let windows = if combined_modules.contains(&ConfigModule::Windows) {
fetch_windows(compositor.as_ref())
} else {
Vec::new()
};
create_and_show_window_impl(applications, compositor, windows, event_tx, cx)
create_and_show_window_impl(applications, compositor, windows, modes, event_tx, cx)
}

fn create_and_show_window_impl(
applications: Vec<ApplicationItem>,
compositor: Arc<dyn Compositor>,
windows: Vec<WindowItem>,
modes: Vec<LauncherMode>,
event_tx: DaemonEventSender,
cx: &mut App,
) -> anyhow::Result<LauncherWindow> {
Expand Down Expand Up @@ -90,7 +92,8 @@ fn create_and_show_window_impl(
let on_hide = move || {
let _ = event_tx.send(DaemonEvent::Window(WindowEvent::RequestHide));
};
let view = cx.new(|cx| LauncherView::new(items, compositor.clone(), on_hide, window, cx));
let view =
cx.new(|cx| LauncherView::new(items, compositor.clone(), modes, on_hide, window, cx));

// Auto-focus the list/search input
view.update(cx, |launcher: &mut LauncherView, cx| {
Expand Down
21 changes: 15 additions & 6 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use anyhow::Result;
use clap::{Parser, Subcommand};

use crate::config::LauncherMode;
use crate::ipc::client;

#[derive(Parser)]
Expand All @@ -14,11 +15,19 @@ pub struct Cli {
#[derive(Subcommand)]
pub enum Commands {
/// Show the launcher window
Show,
Show {
/// Modes to enable (can specify multiple with commas or repeated flags)
#[arg(short, long, value_delimiter = ',')]
modes: Option<Vec<LauncherMode>>,
},
/// Hide the launcher window
Hide,
/// Toggle the launcher window visibility
Toggle,
Toggle {
/// Modes to enable (can specify multiple with commas or repeated flags)
#[arg(short, long, value_delimiter = ',')]
modes: Option<Vec<LauncherMode>>,
},
/// Quit the daemon
Quit,
/// Reload the daemon (fully restart the process)
Expand Down Expand Up @@ -48,14 +57,14 @@ pub fn handle_client_command(cmd: Commands) -> Result<()> {
}

match cmd {
Commands::Show => {
client::show()?;
Commands::Show { modes } => {
client::show(modes)?;
}
Commands::Hide => {
client::hide()?;
}
Commands::Toggle => {
client::toggle()?;
Commands::Toggle { modes } => {
client::toggle(modes)?;
}
Commands::Quit => {
client::quit()?;
Expand Down
18 changes: 10 additions & 8 deletions src/clipboard/copy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,30 @@

use arboard::Clipboard;

use crate::error::ClipboardError;

/// Copy text to the system clipboard.
///
/// Returns `Ok(())` on success, or an error message on failure.
pub fn copy_to_clipboard(text: &str) -> Result<(), String> {
/// Returns `Ok(())` on success, or a `ClipboardError` on failure.
pub fn copy_to_clipboard(text: &str) -> Result<(), ClipboardError> {
let mut clipboard =
Clipboard::new().map_err(|e| format!("Failed to access clipboard: {}", e))?;
Clipboard::new().map_err(|e| ClipboardError::AccessFailed(e.to_string()))?;

clipboard
.set_text(text.to_string())
.map_err(|e| format!("Failed to copy to clipboard: {}", e))
.map_err(|e| ClipboardError::CopyFailed(e.to_string()))
}

/// Copy an RGBA image to the system clipboard.
///
/// Returns `Ok(())` on success, or an error message on failure.
/// Returns `Ok(())` on success, or a `ClipboardError` on failure.
pub fn copy_image_to_clipboard(
width: usize,
height: usize,
rgba_bytes: &[u8],
) -> Result<(), String> {
) -> Result<(), ClipboardError> {
let mut clipboard =
Clipboard::new().map_err(|e| format!("Failed to access clipboard: {}", e))?;
Clipboard::new().map_err(|e| ClipboardError::AccessFailed(e.to_string()))?;

let image_data = arboard::ImageData {
width,
Expand All @@ -33,5 +35,5 @@ pub fn copy_image_to_clipboard(

clipboard
.set_image(image_data)
.map_err(|e| format!("Failed to copy image to clipboard: {}", e))
.map_err(|e| ClipboardError::CopyFailed(e.to_string()))
}
Loading