Skip to content
Open
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
32 changes: 32 additions & 0 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Piri is a high-performance [Niri](https://github.com/YaLTeR/niri) extension tool
- 🔒 **Singleton**: Single-instance assurance. Ensures specific applications remain globally unique, supporting quick focus or automatic process launching (see [Singleton Docs](docs/en/plugins/singleton.md))
- 📋 **Window Order**: Intelligent reordering. Automatically reorders tiled windows based on configured weights, preserving relative positions for identical weights to minimize movement (see [Window Order Docs](docs/en/plugins/window_order.md))
- 🍽️ **Swallow**: Window swallowing mechanism. Automatically hides parent windows when child windows are opened, allowing child windows to replace parent windows in the layout (see [Swallow Docs](docs/en/plugins/swallow.md))
- 🖥️ **Fullscreen**: Fullscreen with position restore. Toggles fullscreen while saving and restoring the window's exact position in the scrolling layout, including shared columns (see [Fullscreen Docs](docs/en/plugins/fullscreen.md))


## Quick Start
Expand Down Expand Up @@ -403,6 +404,37 @@ child_app_id = [".*preview.*", ".*markdown.*"]

For detailed documentation, please refer to the [Swallow documentation](docs/en/plugins/swallow.md).

### Fullscreen

Toggle fullscreen while saving and restoring the window's exact position in the scrolling layout. Niri's built-in fullscreen does not remember where a window was — this plugin does, including windows that share a column with other windows.

**Configuration Example**:
```toml
[piri.plugins]
fullscreen = true
```

**Quick Usage**:
```bash
# Toggle fullscreen with position restore
piri fullscreen-toggle
```

**Niri Keybinding**:
```kdl
binds {
Mod+F { spawn "piri" "fullscreen-toggle"; }
}
```

**Features**:
- Saves and restores column index and row position
- Handles shared columns intelligently (expel, move, consume back)
- Floating windows are fullscreened normally without position tracking
- Falls back to plain fullscreen if position data is unavailable

For detailed documentation, please refer to the [Fullscreen documentation](docs/en/plugins/fullscreen.md).

## Documentation

- [Architecture](docs/en/architecture.md) - Project architecture and how it works
Expand Down
63 changes: 63 additions & 0 deletions docs/en/plugins/fullscreen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Fullscreen Plugin

The Fullscreen plugin provides a fullscreen toggle that saves and restores the window's position in the scrolling layout. Niri's built-in fullscreen does not remember where a window was before going fullscreen — this plugin does.

## Configuration

```toml
[piri.plugins]
fullscreen = true
```

No additional configuration is needed.

## Usage

```bash
# Toggle fullscreen with position restore
piri fullscreen-toggle
```

### Niri Keybinding

Add a keybinding in your niri config (`~/.config/niri/config.kdl`):

```kdl
binds {
Mod+F { spawn "piri" "fullscreen-toggle"; }
}
```

## How It Works

1. **Position Tracking**: The plugin continuously tracks the position (column and row) of all tiled windows in the scrolling layout
2. **Enter Fullscreen**: When toggling fullscreen on, the plugin saves the window's current column index, row index, and whether it was sharing a column with other windows
3. **Exit Fullscreen**: When toggling fullscreen off, the plugin restores the window to its saved position:
- Moves the window back to its original column index
- If the window was sharing a column, consumes it back into that column and restores its row position
4. **Floating Windows**: Floating windows are fullscreened normally without position tracking (they have no column position)

### Shared Column Handling

A key feature is the intelligent handling of shared columns (multiple windows stacked in a single column):

- Before fullscreen, the plugin detects whether the window shares its column with other windows
- On restore, if the column was shared, the window is expelled to its own column, moved to the correct index, then consumed back into the original column at the correct row

## Features

- **Transparent position tracking**: No user action needed, positions are tracked automatically via niri events
- **Shared column support**: Correctly restores windows that were stacked with other windows in the same column
- **Floating-aware**: Floating windows are fullscreened without attempting position restore
- **Robust**: Falls back to plain fullscreen toggle if position data is unavailable

## Use Cases

- Fullscreen a window for focused work, then restore it to its exact position in the layout
- Works seamlessly with complex multi-column, multi-row layouts

## Notes

1. **Use this instead of niri's built-in fullscreen** if you want position restoration. Niri's native `maximize-column` or `fullscreen-window` actions do not restore the previous layout position
2. **Column index is 1-based**: Positions follow niri's internal indexing
3. **Brief delays**: The plugin inserts small delays (50ms) between niri commands during restoration to ensure layout operations complete correctly
4 changes: 4 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ pub struct PluginsConfig {
pub swallow: Option<bool>,
#[serde(default)]
pub workspace_rule: Option<bool>,
#[serde(default)]
pub fullscreen: Option<bool>,
#[serde(rename = "empty_config", default)]
pub empty_config: Option<EmptyPluginConfig>,
}
Expand All @@ -197,6 +199,7 @@ impl Default for PluginsConfig {
window_order: None,
swallow: None,
workspace_rule: None,
fullscreen: None,
empty_config: None,
}
}
Expand Down Expand Up @@ -379,6 +382,7 @@ impl PluginsConfig {
"window_order" => self.window_order.unwrap_or(false),
"swallow" => self.swallow.unwrap_or(false),
"workspace_rule" => self.workspace_rule.unwrap_or(false),
"fullscreen" => self.fullscreen.unwrap_or(false),
_ => false,
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub enum IpcRequest {
name: String,
},
WindowOrderToggle,
FullscreenToggle,
Ping,
Shutdown,
}
Expand Down Expand Up @@ -261,6 +262,9 @@ pub async fn handle_request(
IpcResponse::Error("WindowOrder plugin is not enabled. Please enable it in the configuration file (piri.plugins.window_order = true).".to_string())
}
}
IpcRequest::FullscreenToggle => {
IpcResponse::Error("Fullscreen plugin is not initialized. Please restart the daemon.".to_string())
}
}
}
};
Expand Down
10 changes: 10 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ enum Commands {
#[command(subcommand)]
action: WindowOrderAction,
},
/// Toggle fullscreen with position restore
FullscreenToggle,
/// Stop the daemon
Stop,
/// Generate shell completion script
Expand Down Expand Up @@ -225,6 +227,14 @@ async fn async_main() -> Result<()> {
}
}
}
Commands::FullscreenToggle => {
let client = IpcClient::new(None);
handle_ipc_response(
client.send_request(IpcRequest::FullscreenToggle).await,
"Fullscreen toggled",
"Failed to toggle fullscreen",
)?;
}
Commands::Stop => {
let client = IpcClient::new(None);
handle_ipc_response(
Expand Down
42 changes: 42 additions & 0 deletions src/niri.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,48 @@ impl NiriIpc {
.await
}

/// Toggle fullscreen on a window
pub async fn fullscreen_window(&self, window_id: u64) -> Result<()> {
self.send_action(Action::FullscreenWindow {
id: Some(window_id),
})
.await
}

/// Expel the focused window from its column (creates a new column)
pub async fn expel_window_from_column(&self) -> Result<()> {
self.send_action(Action::ExpelWindowFromColumn {}).await
}

/// Move the focused column to a specific index (1-based)
pub async fn move_column_to_index(&self, index: usize) -> Result<()> {
self.send_action(Action::MoveColumnToIndex { index }).await
}

/// Move the focused window up within its column
pub async fn move_window_up(&self) -> Result<()> {
self.send_action(Action::MoveWindowUp {}).await
}

/// Move the focused window down within its column
pub async fn move_window_down(&self) -> Result<()> {
self.send_action(Action::MoveWindowDown {}).await
}

/// Consume or expel window to the left
#[allow(dead_code)]
pub async fn consume_or_expel_window_left(&self, window_id: Option<u64>) -> Result<()> {
self.send_action(Action::ConsumeOrExpelWindowLeft { id: window_id })
.await
}

/// Consume or expel window to the right
#[allow(dead_code)]
pub async fn consume_or_expel_window_right(&self, window_id: Option<u64>) -> Result<()> {
self.send_action(Action::ConsumeOrExpelWindowRight { id: window_id })
.await
}

/// Get output dimensions (width and height) for focused output
pub async fn get_output_size(&self) -> Result<(u32, u32)> {
let output = self.get_focused_output().await?;
Expand Down
Loading