mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-02-16 10:17:41 +01:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffa76ea28c | ||
|
|
2c00d79968 | ||
|
|
78177af6b8 | ||
|
|
86e0d40828 | ||
|
|
48f6ac8964 | ||
|
|
1db572f789 | ||
|
|
3dad77533b | ||
|
|
f40fb9a251 | ||
|
|
b4e16e43e9 | ||
|
|
4c2e8ff6d2 | ||
|
|
c879aae1e7 | ||
|
|
7e87e83189 | ||
|
|
aae9338f66 | ||
|
|
f68a709f1d | ||
|
|
c76846ac63 | ||
|
|
b29dd8b1d1 | ||
|
|
59c3c14731 | ||
|
|
db96f2cc5a | ||
|
|
a37a6752a8 | ||
|
|
4d0df9c5b5 | ||
|
|
f8ea62f857 | ||
|
|
93bb41737b | ||
|
|
280352eeef | ||
|
|
7619b9b4ed | ||
|
|
72a4d5276e | ||
|
|
4bb3b83d57 | ||
|
|
ccd2f3a464 | ||
|
|
7e242ada66 | ||
|
|
5b2acd0f12 | ||
|
|
ec0bbaae98 | ||
|
|
3c44f3bfdb | ||
|
|
7839980ddf | ||
|
|
6416c0b6eb | ||
|
|
f6ccec9505 | ||
|
|
21cb5e1e6f | ||
|
|
009c0dcd28 | ||
|
|
98c5ab3b9b | ||
|
|
d4eeec994f | ||
|
|
e9ed1cfd3b | ||
|
|
4a2eb391f7 | ||
|
|
41e18bccc6 | ||
|
|
3d373b3630 | ||
|
|
b4e61b079c | ||
|
|
eec6312a51 |
2
.github/workflows/feature-check.yaml
vendored
2
.github/workflows/feature-check.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check and close feature issues
|
||||
uses: actions/github-script@v7
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
|
||||
16
.github/workflows/windows.yaml
vendored
16
.github/workflows/windows.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
cargo-deny:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
RUSTFLAGS: -Ctarget-feature=+crt-static -Dwarnings
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: rustup toolchain install stable --profile minimal
|
||||
@@ -81,12 +81,12 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: echo "VERSION=nightly" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
- run: |
|
||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||
@@ -128,14 +128,14 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: |
|
||||
TAG=${{ github.event.release.tag_name }}
|
||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
- run: |
|
||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||
@@ -170,14 +170,14 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- shell: bash
|
||||
run: |
|
||||
TAG=${{ github.ref_name }}
|
||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
- run: |
|
||||
Compress-Archive -Force ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/x86_64-pc-windows-msvc/release/*.exe komorebi-$Env:VERSION-x86_64-pc-windows-msvc.zip
|
||||
Copy-Item ./komorebi-x86_64-pc-windows-msvc-${{ github.sha }}/wix/*x86_64.msi -Destination ./komorebi-$Env:VERSION-x86_64.msi
|
||||
|
||||
1794
Cargo.lock
generated
1794
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
13
Cargo.toml
@@ -9,7 +9,7 @@ members = [
|
||||
"komorebic-no-console",
|
||||
"komorebi-bar",
|
||||
"komorebi-themes",
|
||||
"komorebi-shortcuts"
|
||||
"komorebi-shortcuts",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
@@ -52,7 +52,7 @@ features = [
|
||||
"Win32_Devices",
|
||||
"Win32_Devices_Display",
|
||||
"Win32_System_Com",
|
||||
"Win32_UI_Shell_Common", # for IObjectArray
|
||||
"Win32_UI_Shell_Common", # for IObjectArray
|
||||
"Win32_Foundation",
|
||||
"Win32_Globalization",
|
||||
"Win32_Graphics_Dwm",
|
||||
@@ -73,5 +73,12 @@ features = [
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_WindowsProgramming",
|
||||
"Media",
|
||||
"Media_Control"
|
||||
"Media_Control",
|
||||
]
|
||||
|
||||
[profile.release-opt]
|
||||
inherits = "release"
|
||||
lto = true
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
|
||||
@@ -394,7 +394,7 @@ every `WindowManagerEvent` and `SocketMessage` handled by `komorebi` in a Rust c
|
||||
Below is a simple example of how to use `komorebi-client` in a basic Rust application.
|
||||
|
||||
```rust
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.37"}
|
||||
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.38"}
|
||||
|
||||
use anyhow::Result;
|
||||
use komorebi_client::Notification;
|
||||
|
||||
@@ -14,7 +14,8 @@ feature-depth = 1
|
||||
ignore = [
|
||||
{ id = "RUSTSEC-2020-0016", reason = "local tcp connectivity is an opt-in feature, and there is no upgrade path for TcpStreamExt" },
|
||||
{ id = "RUSTSEC-2024-0436", reason = "paste being unmaintained is not an issue in our use" },
|
||||
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" }
|
||||
{ id = "RUSTSEC-2024-0320", reason = "not using any yaml features from this library" },
|
||||
{ id = "RUSTSEC-2025-0056", reason = "only used for colour palette generation" }
|
||||
]
|
||||
|
||||
[licenses]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ Usage: komorebic.exe change-layout <DEFAULT_LAYOUT>
|
||||
|
||||
Arguments:
|
||||
<DEFAULT_LAYOUT>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -13,7 +13,7 @@ Arguments:
|
||||
The number of window containers on-screen required to trigger this layout rule
|
||||
|
||||
<LAYOUT>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -10,7 +10,7 @@ Arguments:
|
||||
Target workspace name
|
||||
|
||||
<VALUE>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
16
docs/cli/scrolling-layout-columns.md
Normal file
16
docs/cli/scrolling-layout-columns.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# scrolling-layout-columns
|
||||
|
||||
```
|
||||
Set the number of visible columns for the Scrolling layout on the focused workspace
|
||||
|
||||
Usage: komorebic.exe scrolling-layout-columns <COUNT>
|
||||
|
||||
Arguments:
|
||||
<COUNT>
|
||||
Desired number of visible columns
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
# toggle-pause
|
||||
|
||||
```
|
||||
Toggle window tiling on the focused workspace
|
||||
Toggle the paused state for all window tiling
|
||||
|
||||
Usage: komorebic.exe toggle-pause
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Arguments:
|
||||
The number of window containers on-screen required to trigger this layout rule
|
||||
|
||||
<LAYOUT>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
@@ -13,7 +13,7 @@ Arguments:
|
||||
Workspace index on the specified monitor (zero-indexed)
|
||||
|
||||
<VALUE>
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack]
|
||||
[possible values: bsp, columns, rows, vertical-stack, horizontal-stack, ultrawide-vertical-stack, grid, right-main-vertical-stack, scrolling]
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
|
||||
31
docs/cli/workspace-work-area-offset.md
Normal file
31
docs/cli/workspace-work-area-offset.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# workspace-work-area-offset
|
||||
|
||||
```
|
||||
Set offsets for a workspace to exclude parts of the work area from tiling
|
||||
|
||||
Usage: komorebic.exe workspace-work-area-offset <MONITOR> <WORKSPACE> <LEFT> <TOP> <RIGHT> <BOTTOM>
|
||||
|
||||
Arguments:
|
||||
<MONITOR>
|
||||
Monitor index (zero-indexed)
|
||||
|
||||
<WORKSPACE>
|
||||
Workspace index (zero-indexed)
|
||||
|
||||
<LEFT>
|
||||
Size of the left work area offset (set right to left * 2 to maintain right padding)
|
||||
|
||||
<TOP>
|
||||
Size of the top work area offset (set bottom to the same value to maintain bottom padding)
|
||||
|
||||
<RIGHT>
|
||||
Size of the right work area offset
|
||||
|
||||
<BOTTOM>
|
||||
Size of the bottom work area offset
|
||||
|
||||
Options:
|
||||
-h, --help
|
||||
Print help
|
||||
|
||||
```
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.37/schema.bar.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.38/schema.bar.json",
|
||||
"monitor": 0,
|
||||
"font_family": "JetBrains Mono",
|
||||
"theme": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.37/schema.json",
|
||||
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.38/schema.json",
|
||||
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
|
||||
"window_hiding_behaviour": "Cloak",
|
||||
"cross_monitor_move_behaviour": "Insert",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-bar"
|
||||
version = "0.1.37"
|
||||
version = "0.1.38"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@@ -22,7 +22,7 @@ font-loader = "0.11"
|
||||
hotwatch = { workspace = true }
|
||||
image = "0.25"
|
||||
lazy_static = { workspace = true }
|
||||
netdev = "0.34"
|
||||
netdev = "0.36"
|
||||
num = "0.4"
|
||||
num-derive = "0.4"
|
||||
num-traits = "0.2"
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::render::Grouping;
|
||||
use crate::render::RenderConfig;
|
||||
use crate::render::RenderExt;
|
||||
use crate::widgets::komorebi::Komorebi;
|
||||
use crate::widgets::komorebi::KomorebiNotificationState;
|
||||
use crate::widgets::komorebi::MonitorInfo;
|
||||
use crate::widgets::widget::BarWidget;
|
||||
use crate::widgets::widget::WidgetConfig;
|
||||
use crate::KomorebiEvent;
|
||||
@@ -47,17 +47,14 @@ use eframe::egui::Visuals;
|
||||
use font_loader::system_fonts;
|
||||
use font_loader::system_fonts::FontPropertyBuilder;
|
||||
use komorebi_client::Colour;
|
||||
use komorebi_client::KomorebiTheme;
|
||||
use komorebi_client::MonitorNotification;
|
||||
use komorebi_client::NotificationEvent;
|
||||
use komorebi_client::PathExt;
|
||||
use komorebi_client::SocketMessage;
|
||||
use komorebi_client::VirtualDesktopNotification;
|
||||
use komorebi_themes::catppuccin_egui;
|
||||
use komorebi_themes::Base16Value;
|
||||
use komorebi_themes::Base16Wrapper;
|
||||
use komorebi_themes::Catppuccin;
|
||||
use komorebi_themes::CatppuccinValue;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use std::cell::RefCell;
|
||||
@@ -128,7 +125,7 @@ fn stop_powershell() -> Result<()> {
|
||||
|
||||
pub fn exec_powershell(cmd: &str) -> Result<()> {
|
||||
if let Some(session_stdin) = SESSION_STDIN.lock().as_mut() {
|
||||
if let Err(e) = writeln!(session_stdin, "{}", cmd) {
|
||||
if let Err(e) = writeln!(session_stdin, "{cmd}") {
|
||||
tracing::error!(error = %e, cmd = cmd, "failed to write command to PowerShell stdin");
|
||||
return Err(e);
|
||||
}
|
||||
@@ -153,7 +150,7 @@ pub struct Komobar {
|
||||
pub disabled: bool,
|
||||
pub config: KomobarConfig,
|
||||
pub render_config: Rc<RefCell<RenderConfig>>,
|
||||
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
|
||||
pub monitor_info: Option<Rc<RefCell<MonitorInfo>>>,
|
||||
pub left_widgets: Vec<Box<dyn BarWidget>>,
|
||||
pub center_widgets: Vec<Box<dyn BarWidget>>,
|
||||
pub right_widgets: Vec<Box<dyn BarWidget>>,
|
||||
@@ -345,7 +342,7 @@ impl Komobar {
|
||||
pub fn apply_config(
|
||||
&mut self,
|
||||
ctx: &Context,
|
||||
previous_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
|
||||
previous_monitor_info: Option<Rc<RefCell<MonitorInfo>>>,
|
||||
) {
|
||||
MAX_LABEL_WIDTH.store(
|
||||
self.config.max_label_width.unwrap_or(400.0) as i32,
|
||||
@@ -374,7 +371,7 @@ impl Komobar {
|
||||
self.config.icon_scale,
|
||||
));
|
||||
|
||||
let mut komorebi_notification_state = previous_notification_state;
|
||||
let mut monitor_info = previous_monitor_info;
|
||||
let mut komorebi_widgets = Vec::new();
|
||||
|
||||
for (idx, widget_config) in self.config.left_widgets.iter().enumerate() {
|
||||
@@ -426,19 +423,18 @@ impl Komobar {
|
||||
komorebi_widgets
|
||||
.into_iter()
|
||||
.for_each(|(mut widget, idx, side)| {
|
||||
match komorebi_notification_state {
|
||||
match monitor_info {
|
||||
None => {
|
||||
komorebi_notification_state =
|
||||
Some(widget.komorebi_notification_state.clone());
|
||||
monitor_info = Some(widget.monitor_info.clone());
|
||||
}
|
||||
Some(ref previous) => {
|
||||
if widget.workspaces.is_some_and(|w| w.enable) {
|
||||
previous.borrow_mut().update_from_config(
|
||||
&widget.komorebi_notification_state.borrow(),
|
||||
);
|
||||
if widget.workspaces.is_some() {
|
||||
previous
|
||||
.borrow_mut()
|
||||
.update_from_self(&widget.monitor_info.borrow());
|
||||
}
|
||||
|
||||
widget.komorebi_notification_state = previous.clone();
|
||||
widget.monitor_info = previous.clone();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,17 +460,17 @@ impl Komobar {
|
||||
MonitorConfigOrIndex::Index(idx) => (*idx, None),
|
||||
};
|
||||
|
||||
let mapped_state = self.komorebi_notification_state.as_ref().map(|state| {
|
||||
let state = state.borrow();
|
||||
let mapped_info = self.monitor_info.as_ref().map(|info| {
|
||||
let monitor = info.borrow();
|
||||
(
|
||||
state.monitor_usr_idx_map.get(&usr_monitor_index).copied(),
|
||||
state.mouse_follows_focus,
|
||||
monitor.monitor_usr_idx_map.get(&usr_monitor_index).copied(),
|
||||
monitor.mouse_follows_focus,
|
||||
)
|
||||
});
|
||||
|
||||
if let Some(state) = mapped_state {
|
||||
self.monitor_index = state.0;
|
||||
self.mouse_follows_focus = state.1;
|
||||
if let Some(info) = mapped_info {
|
||||
self.monitor_index = info.0;
|
||||
self.mouse_follows_focus = info.1;
|
||||
}
|
||||
|
||||
if let Some(monitor_index) = self.monitor_index {
|
||||
@@ -526,7 +522,7 @@ impl Komobar {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if self.komorebi_notification_state.is_some() && !self.disabled {
|
||||
} else if self.monitor_info.is_some() && !self.disabled {
|
||||
tracing::warn!("couldn't find the monitor index of this bar! Disabling the bar until the monitor connects...");
|
||||
self.disabled = true;
|
||||
} else {
|
||||
@@ -566,7 +562,7 @@ impl Komobar {
|
||||
|
||||
tracing::info!("widget configuration options applied");
|
||||
|
||||
self.komorebi_notification_state = komorebi_notification_state;
|
||||
self.monitor_info = monitor_info;
|
||||
}
|
||||
|
||||
/// Updates the `size_rect` field. Returns a bool indicating if the field was changed or not
|
||||
@@ -635,8 +631,7 @@ impl Komobar {
|
||||
|
||||
assert!(
|
||||
home.is_dir(),
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||
home_path
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
|
||||
);
|
||||
|
||||
home
|
||||
@@ -650,26 +645,6 @@ impl Komobar {
|
||||
match komorebi_client::StaticConfig::read(&config) {
|
||||
Ok(config) => {
|
||||
if let Some(theme) = config.theme {
|
||||
let stack_accent = match theme {
|
||||
KomorebiTheme::Catppuccin {
|
||||
name, stack_border, ..
|
||||
} => stack_border
|
||||
.unwrap_or(CatppuccinValue::Green)
|
||||
.color32(name.as_theme()),
|
||||
KomorebiTheme::Base16 {
|
||||
name, stack_border, ..
|
||||
} => stack_border
|
||||
.unwrap_or(Base16Value::Base0B)
|
||||
.color32(Base16Wrapper::Base16(name)),
|
||||
KomorebiTheme::Custom {
|
||||
ref colours,
|
||||
stack_border,
|
||||
..
|
||||
} => stack_border
|
||||
.unwrap_or(Base16Value::Base0B)
|
||||
.color32(Base16Wrapper::Custom(colours.clone())),
|
||||
};
|
||||
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(theme),
|
||||
@@ -679,10 +654,6 @@ impl Komobar {
|
||||
bar_grouping,
|
||||
self.render_config.clone(),
|
||||
);
|
||||
|
||||
if let Some(state) = &self.komorebi_notification_state {
|
||||
state.borrow_mut().stack_accent = Some(stack_accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
@@ -725,7 +696,7 @@ impl Komobar {
|
||||
disabled: false,
|
||||
config,
|
||||
render_config: Rc::new(RefCell::new(RenderConfig::new())),
|
||||
komorebi_notification_state: None,
|
||||
monitor_info: None,
|
||||
left_widgets: vec![],
|
||||
center_widgets: vec![],
|
||||
right_widgets: vec![],
|
||||
@@ -871,12 +842,12 @@ impl eframe::App for Komobar {
|
||||
|
||||
if self.scale_factor != ctx.native_pixels_per_point().unwrap_or(1.0) {
|
||||
self.scale_factor = ctx.native_pixels_per_point().unwrap_or(1.0);
|
||||
self.apply_config(ctx, self.komorebi_notification_state.clone());
|
||||
self.apply_config(ctx, self.monitor_info.clone());
|
||||
}
|
||||
|
||||
if let Ok(updated_config) = self.rx_config.try_recv() {
|
||||
self.config = updated_config;
|
||||
self.apply_config(ctx, self.komorebi_notification_state.clone());
|
||||
self.apply_config(ctx, self.monitor_info.clone());
|
||||
}
|
||||
|
||||
match self.rx_gui.try_recv() {
|
||||
@@ -1001,24 +972,26 @@ impl eframe::App for Komobar {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(komorebi_notification_state) = &self.komorebi_notification_state {
|
||||
komorebi_notification_state
|
||||
.borrow_mut()
|
||||
.handle_notification(
|
||||
ctx,
|
||||
self.monitor_index,
|
||||
notification,
|
||||
self.bg_color.clone(),
|
||||
self.bg_color_with_alpha.clone(),
|
||||
self.config.transparency_alpha,
|
||||
self.config.grouping,
|
||||
self.config.theme.clone(),
|
||||
self.render_config.clone(),
|
||||
);
|
||||
if let Some(monitor_info) = &self.monitor_info {
|
||||
monitor_info.borrow_mut().update(
|
||||
self.monitor_index,
|
||||
notification.state,
|
||||
self.render_config.borrow().show_all_icons,
|
||||
);
|
||||
handle_notification(
|
||||
ctx,
|
||||
notification.event,
|
||||
self.bg_color.clone(),
|
||||
self.bg_color_with_alpha.clone(),
|
||||
self.config.transparency_alpha,
|
||||
self.config.grouping,
|
||||
self.config.theme.clone(),
|
||||
self.render_config.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
if should_apply_config {
|
||||
self.apply_config(ctx, self.komorebi_notification_state.clone());
|
||||
self.apply_config(ctx, self.monitor_info.clone());
|
||||
|
||||
// Reposition the Bar
|
||||
self.position_bar();
|
||||
@@ -1344,3 +1317,64 @@ pub enum Alignment {
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn handle_notification(
|
||||
ctx: &Context,
|
||||
event: komorebi_client::NotificationEvent,
|
||||
bg_color: Rc<RefCell<Color32>>,
|
||||
bg_color_with_alpha: Rc<RefCell<Color32>>,
|
||||
transparency_alpha: Option<u8>,
|
||||
grouping: Option<Grouping>,
|
||||
default_theme: Option<KomobarTheme>,
|
||||
render_config: Rc<RefCell<RenderConfig>>,
|
||||
) {
|
||||
if let NotificationEvent::Socket(message) = event {
|
||||
match message {
|
||||
SocketMessage::ReloadStaticConfiguration(path) => {
|
||||
if let Ok(config) = komorebi_client::StaticConfig::read(&path) {
|
||||
if let Some(theme) = config.theme {
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(theme),
|
||||
bg_color.clone(),
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("applied theme from updated komorebi.json");
|
||||
} else if let Some(default_theme) = default_theme {
|
||||
apply_theme(
|
||||
ctx,
|
||||
default_theme,
|
||||
bg_color.clone(),
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!(
|
||||
"removed theme from updated komorebi.json and applied default theme"
|
||||
);
|
||||
} else {
|
||||
tracing::warn!("theme was removed from updated komorebi.json but there was no default theme to apply");
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::Theme(theme) => {
|
||||
apply_theme(
|
||||
ctx,
|
||||
KomobarTheme::from(*theme),
|
||||
bg_color,
|
||||
bg_color_with_alpha.clone(),
|
||||
transparency_alpha,
|
||||
grouping,
|
||||
render_config,
|
||||
);
|
||||
tracing::info!("applied theme from komorebi socket message");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.37`
|
||||
/// The `komorebi.bar.json` configuration file reference for `v0.1.38`
|
||||
pub struct KomobarConfig {
|
||||
/// Bar height (default: 50)
|
||||
pub height: Option<f32>,
|
||||
@@ -332,6 +332,7 @@ pub fn get_individual_spacing(
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum MouseMessage {
|
||||
/// Send a message to the komorebi client.
|
||||
/// By default, a batch of messages are sent in the following order:
|
||||
@@ -367,12 +368,10 @@ pub enum MouseMessage {
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[serde(untagged)]
|
||||
Komorebi(KomorebiMouseMessage),
|
||||
/// Execute a custom command.
|
||||
/// CMD (%variable%), Bash ($variable) and PowerShell ($Env:variable) variables will be resolved.
|
||||
/// Example: `komorebic toggle-pause`
|
||||
#[serde(untagged)]
|
||||
Command(String),
|
||||
}
|
||||
|
||||
|
||||
@@ -159,8 +159,7 @@ fn main() -> color_eyre::Result<()> {
|
||||
|
||||
assert!(
|
||||
home.is_dir(),
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||
home_path
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
|
||||
);
|
||||
|
||||
home
|
||||
|
||||
@@ -181,7 +181,7 @@ impl BarWidget for Battery {
|
||||
.args(["/C", "start", "ms-settings:batterysaver"])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
eprintln!("{error}")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -76,8 +76,8 @@ impl Cpu {
|
||||
|
||||
CpuOutput {
|
||||
label: match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {}%", used),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", used),
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {used}%"),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{used}%"),
|
||||
},
|
||||
selected,
|
||||
}
|
||||
@@ -124,7 +124,7 @@ impl BarWidget for Cpu {
|
||||
if let Err(error) =
|
||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
eprintln!("{error}")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -166,7 +166,7 @@ impl Date {
|
||||
.to_string()
|
||||
.trim()
|
||||
.to_string(),
|
||||
Err(_) => format!("Invalid timezone: {}", timezone),
|
||||
Err(_) => format!("Invalid timezone: {timezone}"),
|
||||
},
|
||||
None => Local::now()
|
||||
.format(&self.format.fmt_string())
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -41,8 +41,7 @@ impl<'de> Deserialize<'de> for KomorebiLayout {
|
||||
let s: String = String::deserialize(deserializer)?;
|
||||
|
||||
// Attempt to deserialize the string as a DefaultLayout
|
||||
if let Ok(default_layout) =
|
||||
from_str::<komorebi_client::DefaultLayout>(&format!("\"{}\"", s))
|
||||
if let Ok(default_layout) = from_str::<komorebi_client::DefaultLayout>(&format!("\"{s}\""))
|
||||
{
|
||||
return Ok(KomorebiLayout::Default(default_layout));
|
||||
}
|
||||
@@ -53,7 +52,7 @@ impl<'de> Deserialize<'de> for KomorebiLayout {
|
||||
"Floating" => Ok(KomorebiLayout::Floating),
|
||||
"Paused" => Ok(KomorebiLayout::Paused),
|
||||
"Custom" => Ok(KomorebiLayout::Custom),
|
||||
_ => Err(Error::custom(format!("Invalid layout: {}", s))),
|
||||
_ => Err(Error::custom(format!("Invalid layout: {s}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,6 +187,12 @@ impl KomorebiLayout {
|
||||
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
}
|
||||
// TODO: @CtByte can you think of a nice icon to draw here?
|
||||
komorebi_client::DefaultLayout::Scrolling => {
|
||||
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
|
||||
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
|
||||
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
|
||||
}
|
||||
},
|
||||
KomorebiLayout::Monocle => {}
|
||||
KomorebiLayout::Floating => {
|
||||
|
||||
@@ -79,9 +79,9 @@ impl Memory {
|
||||
MemoryOutput {
|
||||
label: match self.label_prefix {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("RAM: {}%", usage)
|
||||
format!("RAM: {usage}%")
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", usage),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{usage}%"),
|
||||
},
|
||||
selected,
|
||||
}
|
||||
@@ -128,7 +128,7 @@ impl BarWidget for Memory {
|
||||
if let Err(error) =
|
||||
Command::new("cmd.exe").args(["/C", "taskmgr.exe"]).spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
eprintln!("{error}")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -314,7 +314,7 @@ impl Network {
|
||||
.clicked()
|
||||
{
|
||||
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
|
||||
eprintln!("{}", error);
|
||||
eprintln!("{error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -535,6 +535,6 @@ enum DataUnit {
|
||||
|
||||
impl fmt::Display for DataUnit {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
write!(f, "{self:?}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ pub struct StorageConfig {
|
||||
pub data_refresh_interval: Option<u64>,
|
||||
/// Display label prefix
|
||||
pub label_prefix: Option<LabelPrefix>,
|
||||
/// Show disks that are read only. (default: false)
|
||||
pub show_read_only_disks: Option<bool>,
|
||||
/// Show removable disks. (default: true)
|
||||
pub show_removable_disks: Option<bool>,
|
||||
/// Select when the current percentage is over this value [[1-100]]
|
||||
pub auto_select_over: Option<u8>,
|
||||
/// Hide when the current percentage is under this value [[1-100]]
|
||||
@@ -38,6 +42,8 @@ impl From<StorageConfig> for Storage {
|
||||
disks: Disks::new_with_refreshed_list(),
|
||||
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
|
||||
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
|
||||
show_read_only_disks: value.show_read_only_disks.unwrap_or(false),
|
||||
show_removable_disks: value.show_removable_disks.unwrap_or(true),
|
||||
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
|
||||
auto_hide_under: value.auto_hide_under.map(|o| o.clamp(1, 100)),
|
||||
last_updated: Instant::now(),
|
||||
@@ -55,6 +61,8 @@ pub struct Storage {
|
||||
disks: Disks,
|
||||
data_refresh_interval: u64,
|
||||
label_prefix: LabelPrefix,
|
||||
show_read_only_disks: bool,
|
||||
show_removable_disks: bool,
|
||||
auto_select_over: Option<u8>,
|
||||
auto_hide_under: Option<u8>,
|
||||
last_updated: Instant,
|
||||
@@ -71,6 +79,12 @@ impl Storage {
|
||||
let mut disks = vec![];
|
||||
|
||||
for disk in &self.disks {
|
||||
if disk.is_read_only() && !self.show_read_only_disks {
|
||||
continue;
|
||||
}
|
||||
if disk.is_removable() && !self.show_removable_disks {
|
||||
continue;
|
||||
}
|
||||
let mount = disk.mount_point();
|
||||
let total = disk.total_space();
|
||||
let available = disk.available_space();
|
||||
@@ -87,7 +101,7 @@ impl Storage {
|
||||
LabelPrefix::Text | LabelPrefix::IconAndText => {
|
||||
format!("{} {}%", mount.to_string_lossy(), percentage)
|
||||
}
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", percentage),
|
||||
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage}%"),
|
||||
},
|
||||
selected,
|
||||
})
|
||||
@@ -151,7 +165,7 @@ impl BarWidget for Storage {
|
||||
])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
eprintln!("{error}")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -209,7 +209,7 @@ impl Time {
|
||||
Some(dt.time()),
|
||||
)
|
||||
}
|
||||
Err(_) => (format!("Invalid timezone: {:?}", timezone), None),
|
||||
Err(_) => (format!("Invalid timezone: {timezone:?}"), None),
|
||||
},
|
||||
None => {
|
||||
let dt = Local::now();
|
||||
|
||||
@@ -148,7 +148,7 @@ impl BarWidget for Update {
|
||||
)])
|
||||
.spawn()
|
||||
{
|
||||
eprintln!("{}", error)
|
||||
eprintln!("{error}")
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-client"
|
||||
version = "0.1.37"
|
||||
version = "0.1.38"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -77,6 +77,7 @@ pub use komorebi::WorkspaceConfig;
|
||||
|
||||
use komorebi::DATA_DIR;
|
||||
|
||||
use std::borrow::Borrow;
|
||||
use std::io::BufReader;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
@@ -94,12 +95,15 @@ pub fn send_message(message: &SocketMessage) -> std::io::Result<()> {
|
||||
stream.write_all(serde_json::to_string(message)?.as_bytes())
|
||||
}
|
||||
|
||||
pub fn send_batch(messages: impl IntoIterator<Item = SocketMessage>) -> std::io::Result<()> {
|
||||
pub fn send_batch<Q>(messages: impl IntoIterator<Item = Q>) -> std::io::Result<()>
|
||||
where
|
||||
Q: Borrow<SocketMessage>,
|
||||
{
|
||||
let socket = DATA_DIR.join(KOMOREBI);
|
||||
let mut stream = UnixStream::connect(socket)?;
|
||||
stream.set_write_timeout(Some(Duration::from_secs(1)))?;
|
||||
let msgs = messages.into_iter().fold(String::new(), |mut s, m| {
|
||||
if let Ok(m_str) = serde_json::to_string(&m) {
|
||||
if let Ok(m_str) = serde_json::to_string(m.borrow()) {
|
||||
s.push_str(&m_str);
|
||||
s.push('\n');
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-gui"
|
||||
version = "0.1.37"
|
||||
version = "0.1.38"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use eframe::egui::ViewportBuilder;
|
||||
use std::path::PathBuf;
|
||||
use whkd_core::HotkeyBinding;
|
||||
use whkd_core::Whkdrc;
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -58,21 +57,18 @@ impl eframe::App for Quicklook {
|
||||
ui.label("Filter");
|
||||
ui.add(
|
||||
eframe::egui::text_edit::TextEdit::singleline(&mut self.filter)
|
||||
.hint_text("Filter by command...")
|
||||
.background_color(ctx.style().visuals.faint_bg_color),
|
||||
);
|
||||
ui.end_row();
|
||||
ui.end_row();
|
||||
|
||||
for binding in &whkdrc.bindings {
|
||||
if is_komorebic_binding(binding) {
|
||||
let keys = binding.keys.join(" + ");
|
||||
if self.filter.is_empty()
|
||||
|| binding.command.contains(&self.filter)
|
||||
{
|
||||
ui.label(keys);
|
||||
ui.label(&binding.command);
|
||||
ui.end_row();
|
||||
}
|
||||
let keys = binding.keys.join(" + ");
|
||||
if self.filter.is_empty() || binding.command.contains(&self.filter)
|
||||
{
|
||||
ui.label(keys);
|
||||
ui.label(&binding.command);
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,7 +96,3 @@ fn main() {
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn is_komorebic_binding(binding: &HotkeyBinding) -> bool {
|
||||
binding.command.starts_with("komorebic")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi-themes"
|
||||
version = "0.1.37"
|
||||
version = "0.1.38"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebi"
|
||||
version = "0.1.37"
|
||||
version = "0.1.38"
|
||||
description = "A tiling window manager for Windows"
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2021"
|
||||
|
||||
@@ -17,5 +17,5 @@ pub enum AnimationPrefix {
|
||||
}
|
||||
|
||||
pub fn new_animation_key(prefix: AnimationPrefix, key: String) -> String {
|
||||
format!("{}:{}", prefix, key)
|
||||
format!("{prefix}:{key}")
|
||||
}
|
||||
|
||||
@@ -392,7 +392,7 @@ impl Border {
|
||||
tracing::error!("failed to update border position {error}");
|
||||
}
|
||||
|
||||
if !rect.is_same_size_as(&old_rect) {
|
||||
if !rect.is_same_size_as(&old_rect) || !rect.has_same_position_as(&old_rect) {
|
||||
if let Some(render_target) = (*border_pointer).render_target.as_ref() {
|
||||
let border_width = (*border_pointer).width;
|
||||
let border_offset = (*border_pointer).offset;
|
||||
|
||||
@@ -255,7 +255,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
let window_kind = if idx != ws.focused_container_idx()
|
||||
|| monitor_idx != focused_monitor_idx
|
||||
{
|
||||
if ws.locked_containers().contains(&idx) {
|
||||
if c.locked() {
|
||||
WindowKind::UnfocusedLocked
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
@@ -356,7 +356,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
};
|
||||
|
||||
if !should_process_notification {
|
||||
tracing::trace!("monitor state matches latest snapshot, skipping notification");
|
||||
tracing::debug!("monitor state matches latest snapshot, skipping notification");
|
||||
continue 'receiver;
|
||||
}
|
||||
|
||||
@@ -563,7 +563,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
|
||||
|| monitor_idx != focused_monitor_idx
|
||||
|| focused_window_hwnd != foreground_window
|
||||
{
|
||||
if ws.locked_containers().contains(&idx) {
|
||||
if c.locked() {
|
||||
WindowKind::UnfocusedLocked
|
||||
} else {
|
||||
WindowKind::Unfocused
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use getset::CopyGetters;
|
||||
use getset::Getters;
|
||||
use getset::Setters;
|
||||
use nanoid::nanoid;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::ring::Ring;
|
||||
use crate::window::Window;
|
||||
use crate::Lockable;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters, CopyGetters, Setters)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Container {
|
||||
#[getset(get = "pub")]
|
||||
id: String,
|
||||
#[serde(default)]
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
locked: bool,
|
||||
windows: Ring<Window>,
|
||||
}
|
||||
|
||||
@@ -22,11 +28,23 @@ impl Default for Container {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: nanoid!(),
|
||||
locked: false,
|
||||
windows: Ring::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Lockable for Container {
|
||||
fn locked(&self) -> bool {
|
||||
self.locked
|
||||
}
|
||||
|
||||
fn set_locked(&mut self, locked: bool) -> &mut Self {
|
||||
self.locked = locked;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Container {
|
||||
pub fn hide(&self, omit: Option<isize>) {
|
||||
for window in self.windows().iter().rev() {
|
||||
@@ -144,6 +162,7 @@ impl Container {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json;
|
||||
|
||||
#[test]
|
||||
fn test_contains_window() {
|
||||
@@ -250,4 +269,40 @@ mod tests {
|
||||
// Should return None since window 4 doesn't exist
|
||||
assert_eq!(container.idx_for_window(4), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserializes_with_missing_locked_field_defaults_to_false() {
|
||||
let json = r#"{
|
||||
"id": "test-1",
|
||||
"windows": { "elements": [], "focused": 0 }
|
||||
}"#;
|
||||
let container: Container = serde_json::from_str(json).expect("Should deserialize");
|
||||
|
||||
assert!(!container.locked());
|
||||
assert_eq!(container.id(), "test-1");
|
||||
assert!(container.windows().is_empty());
|
||||
|
||||
let json = r#"{
|
||||
"id": "test-2",
|
||||
"windows": { "elements": [ { "hwnd": 5 }, { "hwnd": 9 } ], "focused": 1 }
|
||||
}"#;
|
||||
let container: Container = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(container.id(), "test-2");
|
||||
assert!(!container.locked());
|
||||
assert_eq!(container.windows(), &[Window::from(5), Window::from(9)]);
|
||||
assert_eq!(container.focused_window_idx(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serializes_and_deserializes() {
|
||||
let mut container = Container::default();
|
||||
container.set_locked(true);
|
||||
|
||||
let serialized = serde_json::to_string(&container).expect("Should serialize");
|
||||
let deserialized: Container =
|
||||
serde_json::from_str(&serialized).expect("Should deserialize");
|
||||
|
||||
assert!(deserialized.locked());
|
||||
assert_eq!(deserialized.id(), container.id());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,10 @@ use super::custom_layout::ColumnSplitWithCapacity;
|
||||
use super::CustomLayout;
|
||||
use super::DefaultLayout;
|
||||
use super::Rect;
|
||||
use crate::default_layout::LayoutOptions;
|
||||
|
||||
pub trait Arrangement {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn calculate(
|
||||
&self,
|
||||
area: &Rect,
|
||||
@@ -21,6 +23,9 @@ pub trait Arrangement {
|
||||
container_padding: Option<i32>,
|
||||
layout_flip: Option<Axis>,
|
||||
resize_dimensions: &[Option<Rect>],
|
||||
focused_idx: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
latest_layout: &[Rect],
|
||||
) -> Vec<Rect>;
|
||||
}
|
||||
|
||||
@@ -33,9 +38,110 @@ impl Arrangement for DefaultLayout {
|
||||
container_padding: Option<i32>,
|
||||
layout_flip: Option<Axis>,
|
||||
resize_dimensions: &[Option<Rect>],
|
||||
focused_idx: usize,
|
||||
layout_options: Option<LayoutOptions>,
|
||||
latest_layout: &[Rect],
|
||||
) -> Vec<Rect> {
|
||||
let len = usize::from(len);
|
||||
let mut dimensions = match self {
|
||||
Self::Scrolling => {
|
||||
let column_count = layout_options
|
||||
.and_then(|o| o.scrolling.map(|s| s.columns))
|
||||
.unwrap_or(3);
|
||||
|
||||
let column_width = area.right / column_count as i32;
|
||||
let mut layouts = Vec::with_capacity(len);
|
||||
|
||||
match len {
|
||||
// treat < 3 windows the same as the columns layout
|
||||
len if len < 3 => {
|
||||
layouts = columns(area, len);
|
||||
|
||||
let adjustment = calculate_columns_adjustment(resize_dimensions);
|
||||
layouts.iter_mut().zip(adjustment.iter()).for_each(
|
||||
|(layout, adjustment)| {
|
||||
layout.top += adjustment.top;
|
||||
layout.bottom += adjustment.bottom;
|
||||
layout.left += adjustment.left;
|
||||
layout.right += adjustment.right;
|
||||
},
|
||||
);
|
||||
|
||||
if matches!(
|
||||
layout_flip,
|
||||
Some(Axis::Horizontal | Axis::HorizontalAndVertical)
|
||||
) {
|
||||
if let 2.. = len {
|
||||
columns_reverse(&mut layouts);
|
||||
}
|
||||
}
|
||||
}
|
||||
// treat >= column_count as scrolling
|
||||
len => {
|
||||
let visible_columns = area.right / column_width;
|
||||
let first_visible: isize = if focused_idx == 0 {
|
||||
// if focused idx is 0, we are at the beginning of the scrolling strip
|
||||
0
|
||||
} else {
|
||||
let previous_first_visible = if latest_layout.is_empty() {
|
||||
0
|
||||
} else {
|
||||
// previous first_visible based on the left position of the first visible window
|
||||
let left_edge = area.left;
|
||||
latest_layout
|
||||
.iter()
|
||||
.position(|rect| rect.left >= left_edge)
|
||||
.unwrap_or(0) as isize
|
||||
};
|
||||
|
||||
let focused_idx = focused_idx as isize;
|
||||
|
||||
if focused_idx < previous_first_visible {
|
||||
// focused window is off the left edge, we need to scroll left
|
||||
focused_idx
|
||||
} else if focused_idx
|
||||
>= previous_first_visible + visible_columns as isize
|
||||
{
|
||||
// focused window is off the right edge, we need to scroll right
|
||||
// and make sure it's the last visible window
|
||||
(focused_idx + 1 - visible_columns as isize).max(0)
|
||||
} else {
|
||||
// focused window is already visible, we don't need to scroll
|
||||
previous_first_visible
|
||||
}
|
||||
.min(
|
||||
(len as isize)
|
||||
.saturating_sub(visible_columns as isize)
|
||||
.max(0),
|
||||
)
|
||||
};
|
||||
|
||||
for i in 0..len {
|
||||
let position = (i as isize) - first_visible;
|
||||
let left = area.left + (position as i32 * column_width);
|
||||
|
||||
layouts.push(Rect {
|
||||
left,
|
||||
top: area.top,
|
||||
right: column_width,
|
||||
bottom: area.bottom,
|
||||
});
|
||||
}
|
||||
|
||||
let adjustment = calculate_scrolling_adjustment(resize_dimensions);
|
||||
layouts.iter_mut().zip(adjustment.iter()).for_each(
|
||||
|(layout, adjustment)| {
|
||||
layout.top += adjustment.top;
|
||||
layout.bottom += adjustment.bottom;
|
||||
layout.left += adjustment.left;
|
||||
layout.right += adjustment.right;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
Self::BSP => recursive_fibonacci(
|
||||
0,
|
||||
len,
|
||||
@@ -487,6 +593,9 @@ impl Arrangement for CustomLayout {
|
||||
container_padding: Option<i32>,
|
||||
_layout_flip: Option<Axis>,
|
||||
_resize_dimensions: &[Option<Rect>],
|
||||
_focused_idx: usize,
|
||||
_layout_options: Option<LayoutOptions>,
|
||||
_latest_layout: &[Rect],
|
||||
) -> Vec<Rect> {
|
||||
let mut dimensions = vec![];
|
||||
let container_count = len.get();
|
||||
@@ -541,7 +650,7 @@ impl Arrangement for CustomLayout {
|
||||
};
|
||||
|
||||
match column {
|
||||
Column::Primary(Option::Some(_)) => {
|
||||
Column::Primary(Some(_)) => {
|
||||
let main_column_area = if idx == 0 {
|
||||
Self::main_column_area(area, primary_right, None)
|
||||
} else {
|
||||
@@ -1115,6 +1224,37 @@ fn calculate_ultrawide_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rec
|
||||
result
|
||||
}
|
||||
|
||||
fn calculate_scrolling_adjustment(resize_dimensions: &[Option<Rect>]) -> Vec<Rect> {
|
||||
let len = resize_dimensions.len();
|
||||
let mut result = vec![Rect::default(); len];
|
||||
|
||||
if len <= 1 {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (i, rect) in resize_dimensions.iter().enumerate() {
|
||||
if let Some(rect) = rect {
|
||||
let is_leftmost = i == 0;
|
||||
let is_rightmost = i == len - 1;
|
||||
|
||||
resize_left(&mut result[i], rect.left);
|
||||
resize_right(&mut result[i], rect.right);
|
||||
resize_top(&mut result[i], rect.top);
|
||||
resize_bottom(&mut result[i], rect.bottom);
|
||||
|
||||
if !is_leftmost && rect.left != 0 {
|
||||
resize_right(&mut result[i - 1], rect.left);
|
||||
}
|
||||
|
||||
if !is_rightmost && rect.right != 0 {
|
||||
resize_left(&mut result[i + 1], rect.right);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn resize_left(rect: &mut Rect, resize: i32) {
|
||||
rect.left += resize / 2;
|
||||
rect.right += -resize / 2;
|
||||
|
||||
@@ -21,9 +21,24 @@ pub enum DefaultLayout {
|
||||
UltrawideVerticalStack,
|
||||
Grid,
|
||||
RightMainVerticalStack,
|
||||
Scrolling,
|
||||
// NOTE: If any new layout is added, please make sure to register the same in `DefaultLayout::cycle`
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct LayoutOptions {
|
||||
/// Options related to the Scrolling layout
|
||||
pub scrolling: Option<ScrollingLayoutOptions>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct ScrollingLayoutOptions {
|
||||
/// Desired number of visible columns (default: 3)
|
||||
pub columns: usize,
|
||||
}
|
||||
|
||||
impl DefaultLayout {
|
||||
pub fn leftmost_index(&self, len: usize) -> usize {
|
||||
match self {
|
||||
@@ -31,6 +46,7 @@ impl DefaultLayout {
|
||||
n if n > 1 => 1,
|
||||
_ => 0,
|
||||
},
|
||||
Self::Scrolling => 0,
|
||||
DefaultLayout::BSP
|
||||
| DefaultLayout::Columns
|
||||
| DefaultLayout::Rows
|
||||
@@ -53,6 +69,7 @@ impl DefaultLayout {
|
||||
_ => len.saturating_sub(1),
|
||||
},
|
||||
DefaultLayout::RightMainVerticalStack => 0,
|
||||
DefaultLayout::Scrolling => len.saturating_sub(1),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +92,7 @@ impl DefaultLayout {
|
||||
| Self::RightMainVerticalStack
|
||||
| Self::HorizontalStack
|
||||
| Self::UltrawideVerticalStack
|
||||
| Self::Scrolling
|
||||
) {
|
||||
return None;
|
||||
};
|
||||
@@ -169,13 +187,15 @@ impl DefaultLayout {
|
||||
Self::HorizontalStack => Self::UltrawideVerticalStack,
|
||||
Self::UltrawideVerticalStack => Self::Grid,
|
||||
Self::Grid => Self::RightMainVerticalStack,
|
||||
Self::RightMainVerticalStack => Self::BSP,
|
||||
Self::RightMainVerticalStack => Self::Scrolling,
|
||||
Self::Scrolling => Self::BSP,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn cycle_previous(self) -> Self {
|
||||
match self {
|
||||
Self::Scrolling => Self::RightMainVerticalStack,
|
||||
Self::RightMainVerticalStack => Self::Grid,
|
||||
Self::Grid => Self::UltrawideVerticalStack,
|
||||
Self::UltrawideVerticalStack => Self::HorizontalStack,
|
||||
|
||||
@@ -102,6 +102,7 @@ impl Direction for DefaultLayout {
|
||||
Self::VerticalStack | Self::RightMainVerticalStack => idx != 0 && idx != 1,
|
||||
Self::UltrawideVerticalStack => idx > 2,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Scrolling => false,
|
||||
},
|
||||
OperationDirection::Down => match self {
|
||||
Self::BSP => idx != count - 1 && idx % 2 != 0,
|
||||
@@ -111,6 +112,7 @@ impl Direction for DefaultLayout {
|
||||
Self::HorizontalStack => idx == 0,
|
||||
Self::UltrawideVerticalStack => idx > 1 && idx != count - 1,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Scrolling => false,
|
||||
},
|
||||
OperationDirection::Left => match self {
|
||||
Self::BSP => idx != 0,
|
||||
@@ -120,6 +122,7 @@ impl Direction for DefaultLayout {
|
||||
Self::HorizontalStack => idx != 0 && idx != 1,
|
||||
Self::UltrawideVerticalStack => idx != 1,
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Scrolling => idx != 0,
|
||||
},
|
||||
OperationDirection::Right => match self {
|
||||
Self::BSP => idx % 2 == 0 && idx != count - 1,
|
||||
@@ -133,6 +136,7 @@ impl Direction for DefaultLayout {
|
||||
_ => idx < 2,
|
||||
},
|
||||
Self::Grid => !is_grid_edge(op_direction, idx, count),
|
||||
Self::Scrolling => idx != count - 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -158,6 +162,7 @@ impl Direction for DefaultLayout {
|
||||
| Self::RightMainVerticalStack => idx - 1,
|
||||
Self::HorizontalStack => 0,
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Scrolling => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +181,7 @@ impl Direction for DefaultLayout {
|
||||
Self::Columns => unreachable!(),
|
||||
Self::HorizontalStack => 1,
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Scrolling => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +209,7 @@ impl Direction for DefaultLayout {
|
||||
_ => 0,
|
||||
},
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Scrolling => idx - 1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,6 +230,7 @@ impl Direction for DefaultLayout {
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Self::Grid => grid_neighbor(op_direction, idx, count),
|
||||
Self::Scrolling => idx + 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![warn(clippy::all)]
|
||||
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
@@ -108,6 +109,7 @@ pub enum SocketMessage {
|
||||
AdjustWorkspacePadding(Sizing, i32),
|
||||
ChangeLayout(DefaultLayout),
|
||||
CycleLayout(CycleDirection),
|
||||
ScrollingLayoutColumns(NonZeroUsize),
|
||||
ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
|
||||
FlipLayout(Axis),
|
||||
ToggleWorkspaceWindowContainerBehaviour,
|
||||
@@ -200,6 +202,7 @@ pub enum SocketMessage {
|
||||
StackbarFontFamily(Option<String>),
|
||||
WorkAreaOffset(Rect),
|
||||
MonitorWorkAreaOffset(usize, Rect),
|
||||
WorkspaceWorkAreaOffset(usize, usize, Rect),
|
||||
ToggleWindowBasedWorkAreaOffset,
|
||||
ResizeDelta(i32),
|
||||
InitialWorkspaceRule(ApplicationIdentifier, String, usize, usize),
|
||||
|
||||
@@ -41,6 +41,10 @@ impl Rect {
|
||||
pub fn is_same_size_as(&self, rhs: &Self) -> bool {
|
||||
self.right == rhs.right && self.bottom == rhs.bottom
|
||||
}
|
||||
|
||||
pub fn has_same_position_as(&self, rhs: &Self) -> bool {
|
||||
self.left == rhs.left && self.top == rhs.top
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
|
||||
@@ -8,7 +8,7 @@ pub mod ring;
|
||||
pub mod container;
|
||||
pub mod core;
|
||||
pub mod focus_manager;
|
||||
pub mod locked_deque;
|
||||
pub mod lockable_sequence;
|
||||
pub mod monitor;
|
||||
pub mod monitor_reconciliator;
|
||||
pub mod process_command;
|
||||
@@ -200,8 +200,7 @@ lazy_static! {
|
||||
|
||||
assert!(
|
||||
home.is_dir(),
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||
home_path
|
||||
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
|
||||
);
|
||||
|
||||
|
||||
@@ -255,6 +254,14 @@ pub static WINDOW_HANDLING_BEHAVIOUR: AtomicCell<WindowHandlingBehaviour> =
|
||||
|
||||
shadow_rs::shadow!(build);
|
||||
|
||||
/// A trait for types that can be marked as locked or unlocked.
|
||||
pub trait Lockable {
|
||||
/// Returns `true` if the item is locked.
|
||||
fn locked(&self) -> bool;
|
||||
/// Sets the locked state of the item.
|
||||
fn set_locked(&mut self, locked: bool) -> &mut Self;
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
@@ -299,7 +306,7 @@ pub fn current_virtual_desktop() -> Option<Vec<u8>> {
|
||||
current
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum NotificationEvent {
|
||||
@@ -316,7 +323,7 @@ pub enum VirtualDesktopNotification {
|
||||
LeftAssociatedVirtualDesktop,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Notification {
|
||||
pub event: NotificationEvent,
|
||||
|
||||
357
komorebi/src/lockable_sequence.rs
Normal file
357
komorebi/src/lockable_sequence.rs
Normal file
@@ -0,0 +1,357 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::Lockable;
|
||||
|
||||
/// A sequence supporting insertion, removal, and swapping of elements while preserving the absolute
|
||||
/// positions of locked items.
|
||||
pub trait LockableSequence<T: Lockable> {
|
||||
/// Inserts a value at `idx`, keeping locked elements at their absolute positions.
|
||||
fn insert_respecting_locks(&mut self, idx: usize, value: T) -> usize;
|
||||
/// Removes the element at `idx`, keeping locked elements at their absolute positions.
|
||||
fn remove_respecting_locks(&mut self, idx: usize) -> Option<T>;
|
||||
/// Swaps the elements at indices `i` and `j`, keeping locked elements at their absolute positions.
|
||||
fn swap_respecting_locks(&mut self, i: usize, j: usize);
|
||||
}
|
||||
|
||||
impl<T: Lockable> LockableSequence<T> for VecDeque<T> {
|
||||
/// Insert `value` at logical index `idx`, with trying to keep locked elements
|
||||
/// (`is_locked()`) anchored at their original positions.
|
||||
///
|
||||
/// Returns the final index of the inserted element.
|
||||
fn insert_respecting_locks(&mut self, mut idx: usize, value: T) -> usize {
|
||||
// 1. Bounds check: if index is out of range, simply append.
|
||||
if idx >= self.len() {
|
||||
self.push_back(value);
|
||||
return self.len() - 1; // last index
|
||||
}
|
||||
|
||||
// 2. Normal VecDeque insertion
|
||||
self.insert(idx, value);
|
||||
|
||||
// 3. Walk left-to-right once, swapping any misplaced locked element. After
|
||||
// the VecDeque::insert all items after `idx` have moved right by one. For every locked
|
||||
// element that is now to the right of an unlocked one, swap it back left exactly once.
|
||||
for index in (idx + 1)..self.len() {
|
||||
if self[index].locked() && !self[index - 1].locked() {
|
||||
self.swap(index - 1, index);
|
||||
|
||||
// If the element we just inserted participated in the swap,
|
||||
// update `idx` so we can return its final location.
|
||||
if idx == index - 1 {
|
||||
idx = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
idx
|
||||
}
|
||||
|
||||
/// Remove element at `idx`, with trying to keep locked elements
|
||||
/// (`is_locked()`) anchored at their original positions.
|
||||
///
|
||||
/// Returns the removed element, or `None` if `idx` is out of bounds.
|
||||
fn remove_respecting_locks(&mut self, idx: usize) -> Option<T> {
|
||||
// 1. Bounds check: if index is out of range, do nothing.
|
||||
if idx >= self.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 2. Remove the element at the requested index.
|
||||
// All elements after idx are now shifted left by 1.
|
||||
let removed = self.remove(idx)?;
|
||||
|
||||
// 3. If less than 2 elements remain, nothing to shift.
|
||||
if self.len() < 2 {
|
||||
return Some(removed);
|
||||
}
|
||||
|
||||
// 4. Iterate from the element just after the removed spot up to the second-to-last
|
||||
// element, right-to-left. This loop "fixes" locked elements that were shifted left
|
||||
// off their anchored positions: If a locked element now has an unlocked element
|
||||
// to its right, swap them back to restore locked order.
|
||||
for index in (idx..self.len() - 1).rev() {
|
||||
// If current is locked and the next one is not locked, swap them.
|
||||
if self[index].locked() && !self[index + 1].locked() {
|
||||
self.swap(index, index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Return the removed value.
|
||||
Some(removed)
|
||||
}
|
||||
|
||||
/// Swaps the elements at indices `i` and `j`, along with their `locked` status, ensuring
|
||||
/// the lock state remains associated with the position rather than the element itself.
|
||||
fn swap_respecting_locks(&mut self, i: usize, j: usize) {
|
||||
self.swap(i, j);
|
||||
let locked_i = self[i].locked();
|
||||
let locked_j = self[j].locked();
|
||||
self[i].set_locked(locked_j);
|
||||
self[j].set_locked(locked_i);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct TestItem {
|
||||
val: i32,
|
||||
locked: bool,
|
||||
}
|
||||
|
||||
impl Lockable for TestItem {
|
||||
fn locked(&self) -> bool {
|
||||
self.locked
|
||||
}
|
||||
|
||||
fn set_locked(&mut self, locked: bool) -> &mut Self {
|
||||
self.locked = locked;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn vals(v: &VecDeque<TestItem>) -> Vec<i32> {
|
||||
v.iter().map(|x| x.val).collect()
|
||||
}
|
||||
|
||||
fn test_deque(items: &[(i32, bool)]) -> VecDeque<TestItem> {
|
||||
items
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|(val, locked)| TestItem { val, locked })
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_respecting_locks() {
|
||||
// Test case 1: Basic insertion with locked index
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
// Insert at index 0, should shift elements while keeping index 2 locked
|
||||
ring.insert_respecting_locks(
|
||||
0,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
// Element '2' remains at index 2, element '1' that was at index 1 is now at index 3
|
||||
assert_eq!(vals(&ring), vec![99, 0, 2, 1, 3, 4]);
|
||||
}
|
||||
|
||||
// Test case 2: Insert at a locked index (should insert after locked)
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
// Try to insert at locked index 2, should insert at index 3 instead
|
||||
let actual_index = ring.insert_respecting_locks(
|
||||
2,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
assert_eq!(actual_index, 3);
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 99, 3, 4]);
|
||||
}
|
||||
|
||||
// Test case 3: Multiple locked indices
|
||||
{
|
||||
// Lock index 1 and 3
|
||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, true), (4, false)]);
|
||||
// Insert at index 0, should maintain locked indices
|
||||
ring.insert_respecting_locks(
|
||||
0,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
// Elements '1' and '3' remain at indices 1 and 3
|
||||
assert_eq!(vals(&ring), vec![99, 1, 0, 3, 2, 4]);
|
||||
}
|
||||
|
||||
// Test case 4: Insert at end
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
let actual_index = ring.insert_respecting_locks(
|
||||
5,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
assert_eq!(actual_index, 5);
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3, 4, 99]);
|
||||
}
|
||||
|
||||
// Test case 5: Empty ring
|
||||
{
|
||||
let mut ring = test_deque(&[]);
|
||||
// Insert into empty deque
|
||||
let actual_index = ring.insert_respecting_locks(
|
||||
0,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
assert_eq!(actual_index, 0);
|
||||
assert_eq!(vals(&ring), vec![99]);
|
||||
}
|
||||
|
||||
// Test case 6: All indices locked
|
||||
{
|
||||
// Lock all indices
|
||||
let mut ring = test_deque(&[(0, true), (1, true), (2, true), (3, true), (4, true)]);
|
||||
// Try to insert at index 2, should insert at the end
|
||||
let actual_index = ring.insert_respecting_locks(
|
||||
2,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
assert_eq!(actual_index, 5);
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3, 4, 99]);
|
||||
}
|
||||
|
||||
// Test case 7: Consecutive locked indices
|
||||
{
|
||||
// Lock index 2 and 3
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, true), (4, false)]);
|
||||
// Insert at index 1, should maintain consecutive locked indices
|
||||
ring.insert_respecting_locks(
|
||||
1,
|
||||
TestItem {
|
||||
val: 99,
|
||||
locked: false,
|
||||
},
|
||||
);
|
||||
// Elements '2' and '3' remain at indices 2 and 3
|
||||
assert_eq!(vals(&ring), vec![0, 99, 2, 3, 1, 4]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_respecting_locks() {
|
||||
// Test case 1: Remove a non-locked index before a locked index
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(0);
|
||||
assert_eq!(removed.map(|x| x.val), Some(0));
|
||||
// Elements '2' remain at index 2
|
||||
assert_eq!(vals(&ring), vec![1, 3, 2, 4]);
|
||||
}
|
||||
|
||||
// Test case 2: Remove a locked index
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(2);
|
||||
assert_eq!(removed.map(|x| x.val), Some(2));
|
||||
// Elements should stay at the same places
|
||||
assert_eq!(vals(&ring), vec![0, 1, 3, 4]);
|
||||
}
|
||||
|
||||
// Test case 3: Remove an index after a locked index
|
||||
{
|
||||
// Lock index 1
|
||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, false), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(3);
|
||||
assert_eq!(removed.map(|x| x.val), Some(3));
|
||||
// Elements should stay at the same places
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 4]);
|
||||
}
|
||||
|
||||
// Test case 4: Multiple locked indices
|
||||
{
|
||||
// Lock index 1 and 3
|
||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, true), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(0);
|
||||
assert_eq!(removed.map(|x| x.val), Some(0));
|
||||
// Elements '1' and '3' remain at indices '1' and '3'
|
||||
assert_eq!(vals(&ring), vec![2, 1, 4, 3]);
|
||||
}
|
||||
|
||||
// Test case 5: Remove the last element
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(4);
|
||||
assert_eq!(removed.map(|x| x.val), Some(4));
|
||||
// Index 2 should still be at the same place
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3]);
|
||||
}
|
||||
|
||||
// Test case 6: Invalid index
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true), (3, false), (4, false)]);
|
||||
let removed = ring.remove_respecting_locks(10);
|
||||
assert_eq!(removed, None);
|
||||
// Deque unchanged
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3, 4]);
|
||||
}
|
||||
|
||||
// Test case 7: Remove enough elements to make a locked index invalid
|
||||
{
|
||||
// Lock index 2
|
||||
let mut ring = test_deque(&[(0, false), (1, false), (2, true)]);
|
||||
ring.remove_respecting_locks(0);
|
||||
// Index 2 should now be '1'
|
||||
assert_eq!(vals(&ring), vec![1, 2]);
|
||||
}
|
||||
|
||||
// Test case 8: Removing an element before multiple locked indices
|
||||
{
|
||||
// Lock index 2 and 4
|
||||
let mut ring = test_deque(&[
|
||||
(0, false),
|
||||
(1, false),
|
||||
(2, true),
|
||||
(3, false),
|
||||
(4, true),
|
||||
(5, false),
|
||||
]);
|
||||
let removed = ring.remove_respecting_locks(1);
|
||||
assert_eq!(removed.map(|x| x.val), Some(1));
|
||||
// Both indices should still be at the same place
|
||||
assert_eq!(vals(&ring), vec![0, 3, 2, 5, 4]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_swap_respecting_locks_various_cases() {
|
||||
// Swap unlocked and locked
|
||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false), (3, false)]);
|
||||
ring.swap_respecting_locks(0, 1);
|
||||
assert_eq!(vals(&ring), vec![1, 0, 2, 3]);
|
||||
assert!(!ring[0].locked);
|
||||
assert!(ring[1].locked);
|
||||
ring.swap_respecting_locks(0, 1);
|
||||
assert_eq!(vals(&ring), vec![0, 1, 2, 3]);
|
||||
assert!(!ring[0].locked);
|
||||
assert!(ring[1].locked);
|
||||
|
||||
// Both locked
|
||||
let mut ring = test_deque(&[(0, true), (1, false), (2, true)]);
|
||||
ring.swap_respecting_locks(0, 2);
|
||||
assert_eq!(vals(&ring), vec![2, 1, 0]);
|
||||
assert!(ring[0].locked);
|
||||
assert!(!ring[1].locked);
|
||||
assert!(ring[2].locked);
|
||||
|
||||
// Both unlocked
|
||||
let mut ring = test_deque(&[(0, false), (1, true), (2, false)]);
|
||||
ring.swap_respecting_locks(0, 2);
|
||||
assert_eq!(vals(&ring), vec![2, 1, 0]);
|
||||
assert!(!ring[0].locked);
|
||||
assert!(ring[1].locked);
|
||||
assert!(!ring[2].locked);
|
||||
}
|
||||
}
|
||||
@@ -1,316 +0,0 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
pub struct LockedDeque<'a, T> {
|
||||
deque: &'a mut VecDeque<T>,
|
||||
locked_indices: &'a mut BTreeSet<usize>,
|
||||
}
|
||||
|
||||
impl<'a, T: PartialEq> LockedDeque<'a, T> {
|
||||
pub fn new(deque: &'a mut VecDeque<T>, locked_indices: &'a mut BTreeSet<usize>) -> Self {
|
||||
Self {
|
||||
deque,
|
||||
locked_indices,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, index: usize, value: T) -> usize {
|
||||
insert_respecting_locks(self.deque, self.locked_indices, index, value)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, index: usize) -> Option<T> {
|
||||
remove_respecting_locks(self.deque, self.locked_indices, index)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_respecting_locks<T>(
|
||||
deque: &mut VecDeque<T>,
|
||||
locked_idx: &mut BTreeSet<usize>,
|
||||
idx: usize,
|
||||
value: T,
|
||||
) -> usize {
|
||||
if idx == deque.len() {
|
||||
deque.push_back(value);
|
||||
return idx;
|
||||
}
|
||||
|
||||
let mut new_deque = VecDeque::with_capacity(deque.len() + 1);
|
||||
let mut temp_locked_deque = VecDeque::new();
|
||||
let mut j = 0;
|
||||
let mut corrected_idx = idx;
|
||||
|
||||
for (i, el) in deque.drain(..).enumerate() {
|
||||
if i == idx {
|
||||
corrected_idx = j;
|
||||
}
|
||||
if locked_idx.contains(&i) {
|
||||
temp_locked_deque.push_back(el);
|
||||
} else {
|
||||
new_deque.push_back(el);
|
||||
j += 1;
|
||||
}
|
||||
}
|
||||
|
||||
new_deque.insert(corrected_idx, value);
|
||||
|
||||
for (locked_el, locked_idx) in temp_locked_deque.into_iter().zip(locked_idx.iter()) {
|
||||
new_deque.insert(*locked_idx, locked_el);
|
||||
if *locked_idx <= corrected_idx {
|
||||
corrected_idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
*deque = new_deque;
|
||||
|
||||
corrected_idx
|
||||
}
|
||||
|
||||
pub fn remove_respecting_locks<T>(
|
||||
deque: &mut VecDeque<T>,
|
||||
locked_idx: &mut BTreeSet<usize>,
|
||||
idx: usize,
|
||||
) -> Option<T> {
|
||||
if idx >= deque.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let final_size = deque.len() - 1;
|
||||
|
||||
let mut new_deque = VecDeque::with_capacity(final_size);
|
||||
let mut temp_locked_deque = VecDeque::new();
|
||||
let mut removed = None;
|
||||
let mut removed_locked_idx = None;
|
||||
|
||||
for (i, el) in deque.drain(..).enumerate() {
|
||||
if i == idx {
|
||||
removed = Some(el);
|
||||
removed_locked_idx = locked_idx.contains(&i).then_some(i);
|
||||
} else if locked_idx.contains(&i) {
|
||||
temp_locked_deque.push_back(el);
|
||||
} else {
|
||||
new_deque.push_back(el);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(i) = removed_locked_idx {
|
||||
let mut above = locked_idx.split_off(&i);
|
||||
above.pop_first();
|
||||
locked_idx.extend(above.into_iter().map(|i| i - 1));
|
||||
}
|
||||
|
||||
while locked_idx.last().is_some_and(|i| *i >= final_size) {
|
||||
locked_idx.pop_last();
|
||||
}
|
||||
|
||||
let extra_invalid_idx = (new_deque.len()
|
||||
..(new_deque.len() + temp_locked_deque.len() - locked_idx.len()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (locked_el, locked_idx) in temp_locked_deque
|
||||
.into_iter()
|
||||
.zip(locked_idx.iter().chain(extra_invalid_idx.iter()))
|
||||
{
|
||||
new_deque.insert(*locked_idx, locked_el);
|
||||
}
|
||||
|
||||
*deque = new_deque;
|
||||
|
||||
removed
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[test]
|
||||
fn test_insert_respecting_locks() {
|
||||
// Test case 1: Basic insertion with locked index
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
// Insert at index 0, should shift elements while keeping index 2 locked
|
||||
insert_respecting_locks(&mut deque, &mut locked, 0, 99);
|
||||
assert_eq!(deque, VecDeque::from(vec![99, 0, 2, 1, 3, 4]));
|
||||
// Element '2' remains at index 2, element '1' that was at index 1 is now at index 3
|
||||
}
|
||||
|
||||
// Test case 2: Insert at a locked index
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
// Try to insert at locked index 2, should insert at index 3 instead
|
||||
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 2, 99);
|
||||
assert_eq!(actual_index, 3);
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 99, 3, 4]));
|
||||
}
|
||||
|
||||
// Test case 3: Multiple locked indices
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(1); // Lock index 1
|
||||
locked.insert(3); // Lock index 3
|
||||
|
||||
// Insert at index 0, should maintain locked indices
|
||||
insert_respecting_locks(&mut deque, &mut locked, 0, 99);
|
||||
assert_eq!(deque, VecDeque::from(vec![99, 1, 0, 3, 2, 4]));
|
||||
// Elements '1' and '3' remain at indices 1 and 3
|
||||
}
|
||||
|
||||
// Test case 4: Insert at end
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
// Insert at end of deque
|
||||
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 5, 99);
|
||||
assert_eq!(actual_index, 5);
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3, 4, 99]));
|
||||
}
|
||||
|
||||
// Test case 5: Empty deque
|
||||
{
|
||||
let mut deque = VecDeque::new();
|
||||
let mut locked = BTreeSet::new();
|
||||
|
||||
// Insert into empty deque
|
||||
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 0, 99);
|
||||
assert_eq!(actual_index, 0);
|
||||
assert_eq!(deque, VecDeque::from(vec![99]));
|
||||
}
|
||||
|
||||
// Test case 6: All indices locked
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
for i in 0..5 {
|
||||
locked.insert(i); // Lock all indices
|
||||
}
|
||||
|
||||
// Try to insert at index 2, should insert at the end
|
||||
let actual_index = insert_respecting_locks(&mut deque, &mut locked, 2, 99);
|
||||
assert_eq!(actual_index, 5);
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3, 4, 99]));
|
||||
}
|
||||
|
||||
// Test case 7: Consecutive locked indices
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
locked.insert(3); // Lock index 3
|
||||
|
||||
// Insert at index 1, should maintain consecutive locked indices
|
||||
insert_respecting_locks(&mut deque, &mut locked, 1, 99);
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 99, 2, 3, 1, 4]));
|
||||
// Elements '2' and '3' remain at indices 2 and 3
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_respecting_locks() {
|
||||
// Test case 1: Remove a non-locked index before a locked index
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
let removed = remove_respecting_locks(&mut deque, &mut locked, 0);
|
||||
assert_eq!(removed, Some(0));
|
||||
assert_eq!(deque, VecDeque::from(vec![1, 3, 2, 4]));
|
||||
assert!(locked.contains(&2)); // Index 2 should still be locked
|
||||
}
|
||||
|
||||
// Test case 2: Remove a locked index
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
let removed = remove_respecting_locks(&mut deque, &mut locked, 2);
|
||||
assert_eq!(removed, Some(2));
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 1, 3, 4]));
|
||||
assert!(!locked.contains(&2)); // Index 2 should be unlocked
|
||||
}
|
||||
|
||||
// Test case 3: Remove an index after a locked index
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(1); // Lock index 1
|
||||
|
||||
let removed = remove_respecting_locks(&mut deque, &mut locked, 3);
|
||||
assert_eq!(removed, Some(3));
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 4]));
|
||||
assert!(locked.contains(&1)); // Index 1 should still be locked
|
||||
}
|
||||
|
||||
// Test case 4: Multiple locked indices
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(1); // Lock index 1
|
||||
locked.insert(3); // Lock index 3
|
||||
|
||||
let removed = remove_respecting_locks(&mut deque, &mut locked, 0);
|
||||
assert_eq!(removed, Some(0));
|
||||
assert_eq!(deque, VecDeque::from(vec![2, 1, 4, 3]));
|
||||
assert!(locked.contains(&1) && locked.contains(&3)); // Both indices should still be locked
|
||||
}
|
||||
|
||||
// Test case 5: Remove the last element
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
let removed = remove_respecting_locks(&mut deque, &mut locked, 4);
|
||||
assert_eq!(removed, Some(4));
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3]));
|
||||
assert!(locked.contains(&2)); // Index 2 should still be locked
|
||||
}
|
||||
|
||||
// Test case 6: Invalid index
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
let removed = remove_respecting_locks(&mut deque, &mut locked, 10);
|
||||
assert_eq!(removed, None);
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 1, 2, 3, 4])); // Deque unchanged
|
||||
assert!(locked.contains(&2)); // Lock unchanged
|
||||
}
|
||||
|
||||
// Test case 7: Remove enough elements to make a locked index invalid
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
|
||||
remove_respecting_locks(&mut deque, &mut locked, 0);
|
||||
assert_eq!(deque, VecDeque::from(vec![1, 2]));
|
||||
assert!(!locked.contains(&2)); // Index 2 should now be invalid
|
||||
}
|
||||
|
||||
// Test case 8: Removing an element before multiple locked indices
|
||||
{
|
||||
let mut deque = VecDeque::from(vec![0, 1, 2, 3, 4, 5]);
|
||||
let mut locked = BTreeSet::new();
|
||||
locked.insert(2); // Lock index 2
|
||||
locked.insert(4); // Lock index 4
|
||||
|
||||
let removed = remove_respecting_locks(&mut deque, &mut locked, 1);
|
||||
assert_eq!(removed, Some(1));
|
||||
assert_eq!(deque, VecDeque::from(vec![0, 3, 2, 5, 4]));
|
||||
assert!(locked.contains(&2) && locked.contains(&4)); // Both indices should still be locked
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -500,7 +500,7 @@ impl Monitor {
|
||||
if workspaces.get(idx).is_none() {
|
||||
workspaces.resize(idx + 1, Workspace::default());
|
||||
}
|
||||
|
||||
self.set_last_focused_workspace(Some(self.workspaces.focused_idx()));
|
||||
self.workspaces.focus(idx);
|
||||
}
|
||||
|
||||
|
||||
@@ -801,7 +801,7 @@ mod tests {
|
||||
let wm = match WindowManager::new(receiver, Some(socket_path.clone())) {
|
||||
Ok(manager) => manager,
|
||||
Err(e) => {
|
||||
panic!("Failed to create WindowManager: {}", e);
|
||||
panic!("Failed to create WindowManager: {e}");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -829,7 +829,7 @@ mod tests {
|
||||
Ok(notification) => {
|
||||
assert_eq!(notification, MonitorNotification::ResolutionScalingChanged);
|
||||
}
|
||||
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
|
||||
Err(e) => panic!("Failed to receive MonitorNotification: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -849,7 +849,7 @@ mod tests {
|
||||
for _ in 0..20 {
|
||||
let notification = match receiver.try_recv() {
|
||||
Ok(notification) => notification,
|
||||
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
|
||||
Err(e) => panic!("Failed to receive MonitorNotification: {e}"),
|
||||
};
|
||||
assert_eq!(
|
||||
notification,
|
||||
@@ -960,7 +960,7 @@ mod tests {
|
||||
Ok(notification) => {
|
||||
assert_eq!(notification, MonitorNotification::DisplayConnectionChange);
|
||||
}
|
||||
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
|
||||
Err(e) => panic!("Failed to receive MonitorNotification: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,8 @@ use crate::core::StateQuery;
|
||||
use crate::core::WindowContainerBehaviour;
|
||||
use crate::core::WindowKind;
|
||||
use crate::current_virtual_desktop;
|
||||
use crate::default_layout::LayoutOptions;
|
||||
use crate::default_layout::ScrollingLayoutOptions;
|
||||
use crate::monitor::MonitorInformation;
|
||||
use crate::notify_subscribers;
|
||||
use crate::stackbar_manager;
|
||||
@@ -207,25 +209,6 @@ impl WindowManager {
|
||||
// We don't have From implemented for &mut WindowManager
|
||||
let initial_state = State::from(self.as_ref());
|
||||
|
||||
match message {
|
||||
SocketMessage::CycleFocusEmptyWorkspace(_)
|
||||
| SocketMessage::CycleFocusWorkspace(_)
|
||||
| SocketMessage::FocusWorkspaceNumber(_) => {
|
||||
if let Some(monitor) = self.focused_monitor_mut() {
|
||||
let idx = monitor.focused_workspace_idx();
|
||||
monitor.set_last_focused_workspace(Option::from(idx));
|
||||
}
|
||||
}
|
||||
SocketMessage::FocusMonitorWorkspaceNumber(target_monitor_idx, _) => {
|
||||
let idx = self.focused_workspace_idx_for_monitor_idx(target_monitor_idx)?;
|
||||
if let Some(monitor) = self.monitors_mut().get_mut(target_monitor_idx) {
|
||||
monitor.set_last_focused_workspace(Option::from(idx));
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let mut force_update_borders = false;
|
||||
match message {
|
||||
SocketMessage::Promote => self.promote_container_to_front()?,
|
||||
@@ -347,7 +330,7 @@ impl WindowManager {
|
||||
SocketMessage::StackWindow(direction) => self.add_window_to_container(direction)?,
|
||||
SocketMessage::UnstackWindow => self.remove_window_from_container()?,
|
||||
SocketMessage::StackAll => self.stack_all()?,
|
||||
SocketMessage::UnstackAll => self.unstack_all()?,
|
||||
SocketMessage::UnstackAll => self.unstack_all(true)?,
|
||||
SocketMessage::CycleStack(direction) => {
|
||||
self.cycle_container_window_in_direction(direction)?;
|
||||
}
|
||||
@@ -391,7 +374,9 @@ impl WindowManager {
|
||||
.get_mut(workspace_idx)
|
||||
.ok_or_eyre("no workspace at the given index")?;
|
||||
|
||||
workspace.locked_containers.insert(container_idx);
|
||||
if let Some(container) = workspace.containers_mut().get_mut(container_idx) {
|
||||
container.set_locked(true);
|
||||
}
|
||||
}
|
||||
SocketMessage::UnlockMonitorWorkspaceContainer(
|
||||
monitor_idx,
|
||||
@@ -408,7 +393,9 @@ impl WindowManager {
|
||||
.get_mut(workspace_idx)
|
||||
.ok_or_eyre("no workspace at the given index")?;
|
||||
|
||||
workspace.locked_containers.remove(&container_idx);
|
||||
if let Some(container) = workspace.containers_mut().get_mut(container_idx) {
|
||||
container.set_locked(false);
|
||||
}
|
||||
}
|
||||
SocketMessage::ToggleLock => self.toggle_lock()?,
|
||||
SocketMessage::ToggleFloat => self.toggle_float(false)?,
|
||||
@@ -933,6 +920,27 @@ impl WindowManager {
|
||||
self.retile_all(true)?
|
||||
}
|
||||
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
|
||||
SocketMessage::ScrollingLayoutColumns(count) => {
|
||||
let focused_workspace = self.focused_workspace_mut()?;
|
||||
|
||||
let options = match focused_workspace.layout_options() {
|
||||
Some(mut opts) => {
|
||||
if let Some(scrolling) = &mut opts.scrolling {
|
||||
scrolling.columns = count.into();
|
||||
}
|
||||
|
||||
opts
|
||||
}
|
||||
None => LayoutOptions {
|
||||
scrolling: Some(ScrollingLayoutOptions {
|
||||
columns: count.into(),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
focused_workspace.set_layout_options(Some(options));
|
||||
self.update_focused_workspace(false, false)?;
|
||||
}
|
||||
SocketMessage::ChangeLayout(layout) => self.change_workspace_layout_default(layout)?,
|
||||
SocketMessage::CycleLayout(direction) => self.cycle_layout(direction)?,
|
||||
SocketMessage::ChangeLayoutCustom(ref path) => {
|
||||
@@ -1751,7 +1759,7 @@ Stop-Process -Name:komorebi-bar -ErrorAction SilentlyContinue
|
||||
{
|
||||
for config_file_path in &mut *display_bar_configurations {
|
||||
let script = r#"Start-Process "komorebi-bar" '"--config" "CONFIGFILE"' -WindowStyle hidden"#
|
||||
.replace("CONFIGFILE", &config_file_path.to_string_lossy());
|
||||
.replace("CONFIGFILE", &config_file_path.to_string_lossy());
|
||||
|
||||
match powershell_script::run(&script) {
|
||||
Ok(_) => {
|
||||
@@ -1878,6 +1886,14 @@ if (!(Get-Process komorebi-bar -ErrorAction SilentlyContinue))
|
||||
self.retile_all(false)?;
|
||||
}
|
||||
}
|
||||
SocketMessage::WorkspaceWorkAreaOffset(monitor_idx, workspace_idx, rect) => {
|
||||
if let Some(monitor) = self.monitors_mut().get_mut(monitor_idx) {
|
||||
if let Some(workspace) = monitor.workspaces_mut().get_mut(workspace_idx) {
|
||||
workspace.set_work_area_offset(Option::from(rect));
|
||||
self.retile_all(false)?
|
||||
}
|
||||
}
|
||||
}
|
||||
SocketMessage::ToggleWindowBasedWorkAreaOffset => {
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
workspace.set_apply_window_based_work_area_offset(
|
||||
|
||||
@@ -25,6 +25,8 @@ use crate::window_manager_event::WindowManagerEvent;
|
||||
use crate::windows_api::WindowsApi;
|
||||
use crate::winevent::WinEvent;
|
||||
use crate::workspace::WorkspaceLayer;
|
||||
use crate::DefaultLayout;
|
||||
use crate::Layout;
|
||||
use crate::Notification;
|
||||
use crate::NotificationEvent;
|
||||
use crate::State;
|
||||
@@ -301,7 +303,11 @@ impl WindowManager {
|
||||
// don't want to trigger the full workspace updates when there are no managed
|
||||
// containers - this makes floating windows on empty workspaces go into very
|
||||
// annoying focus change loops which prevents users from interacting with them
|
||||
if !self.focused_workspace()?.containers().is_empty() {
|
||||
if !matches!(
|
||||
self.focused_workspace()?.layout(),
|
||||
Layout::Default(DefaultLayout::Scrolling)
|
||||
) && !self.focused_workspace()?.containers().is_empty()
|
||||
{
|
||||
self.update_focused_workspace(self.mouse_follows_focus, false)?;
|
||||
}
|
||||
|
||||
@@ -328,6 +334,14 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
workspace.set_layer(WorkspaceLayer::Tiling);
|
||||
|
||||
if matches!(
|
||||
self.focused_workspace()?.layout(),
|
||||
Layout::Default(DefaultLayout::Scrolling)
|
||||
) && !self.focused_workspace()?.containers().is_empty()
|
||||
{
|
||||
self.update_focused_workspace(self.mouse_follows_focus, false)?;
|
||||
}
|
||||
}
|
||||
Some(idx) => {
|
||||
if let Some(_window) = workspace.floating_windows().get(idx) {
|
||||
|
||||
@@ -43,10 +43,6 @@ impl<T> Ring<T> {
|
||||
pub fn focused_mut(&mut self) -> Option<&mut T> {
|
||||
self.elements.get_mut(self.focused)
|
||||
}
|
||||
|
||||
pub fn swap(&mut self, i: usize, j: usize) {
|
||||
self.elements.swap(i, j);
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_ring_elements {
|
||||
|
||||
@@ -28,7 +28,7 @@ pub static STACKBAR_UNFOCUSED_TEXT_COLOUR: AtomicU32 = AtomicU32::new(11776947);
|
||||
pub static STACKBAR_TAB_BACKGROUND_COLOUR: AtomicU32 = AtomicU32::new(3355443); // gray
|
||||
pub static STACKBAR_TAB_HEIGHT: AtomicI32 = AtomicI32::new(40);
|
||||
pub static STACKBAR_TAB_WIDTH: AtomicI32 = AtomicI32::new(200);
|
||||
pub static STACKBAR_LABEL: AtomicCell<StackbarLabel> = AtomicCell::new(StackbarLabel::Process);
|
||||
pub static STACKBAR_LABEL: AtomicCell<StackbarLabel> = AtomicCell::new(StackbarLabel::Title);
|
||||
pub static STACKBAR_MODE: AtomicCell<StackbarMode> = AtomicCell::new(StackbarMode::Never);
|
||||
|
||||
pub static STACKBAR_TEMPORARILY_DISABLED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
@@ -58,15 +58,19 @@ use windows::Win32::UI::WindowsAndMessaging::CreateWindowExW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DefWindowProcW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::LoadCursorW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetCursor;
|
||||
use windows::Win32::UI::WindowsAndMessaging::SetLayeredWindowAttributes;
|
||||
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CS_HREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::CS_VREDRAW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::IDC_ARROW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::LWA_COLORKEY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::MSG;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_LBUTTONDOWN;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WM_SETCURSOR;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED;
|
||||
use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOOLWINDOW;
|
||||
@@ -312,6 +316,16 @@ impl Stackbar {
|
||||
) -> LRESULT {
|
||||
unsafe {
|
||||
match msg {
|
||||
WM_SETCURSOR => match LoadCursorW(None, IDC_ARROW) {
|
||||
Ok(cursor) => {
|
||||
SetCursor(Some(cursor));
|
||||
LRESULT(0)
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!("{error}");
|
||||
LRESULT(1)
|
||||
}
|
||||
},
|
||||
WM_LBUTTONDOWN => {
|
||||
let stackbars_containers = STACKBARS_CONTAINERS.lock();
|
||||
if let Some(container) = stackbars_containers.get(&(hwnd.0 as isize)) {
|
||||
|
||||
@@ -35,6 +35,7 @@ use crate::core::StackbarMode;
|
||||
use crate::core::WindowContainerBehaviour;
|
||||
use crate::core::WindowManagementBehaviour;
|
||||
use crate::current_virtual_desktop;
|
||||
use crate::default_layout::LayoutOptions;
|
||||
use crate::monitor;
|
||||
use crate::monitor::Monitor;
|
||||
use crate::monitor_reconciliator;
|
||||
@@ -191,6 +192,9 @@ pub struct WorkspaceConfig {
|
||||
/// Layout (default: BSP)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layout: Option<DefaultLayout>,
|
||||
/// Layout-specific options (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layout_options: Option<LayoutOptions>,
|
||||
/// END OF LIFE FEATURE: Custom Layout (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde_as(as = "Option<ResolvedPathBuf>")]
|
||||
@@ -214,6 +218,9 @@ pub struct WorkspaceConfig {
|
||||
/// Permanent workspace application rules
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub workspace_rules: Option<Vec<MatchingRule>>,
|
||||
/// Workspace specific work area offset (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub work_area_offset: Option<Rect>,
|
||||
/// Apply this monitor's window-based work area offset (default: true)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub apply_window_based_work_area_offset: Option<bool>,
|
||||
@@ -226,6 +233,9 @@ pub struct WorkspaceConfig {
|
||||
/// Enable or disable float override, which makes it so every new window opens in floating mode (default: false)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub float_override: Option<bool>,
|
||||
/// Enable or disable tiling for the workspace (default: true)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tile: Option<bool>,
|
||||
/// Specify an axis on which to flip the selected layout (default: None)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layout_flip: Option<Axis>,
|
||||
@@ -274,6 +284,8 @@ impl From<&Workspace> for WorkspaceConfig {
|
||||
}
|
||||
});
|
||||
|
||||
let tile = if *value.tile() { None } else { Some(false) };
|
||||
|
||||
Self {
|
||||
name: value
|
||||
.name()
|
||||
@@ -286,6 +298,7 @@ impl From<&Workspace> for WorkspaceConfig {
|
||||
Layout::Custom(_) => None,
|
||||
})
|
||||
.flatten(),
|
||||
layout_options: value.layout_options(),
|
||||
custom_layout: value
|
||||
.workspace_config()
|
||||
.as_ref()
|
||||
@@ -305,10 +318,12 @@ impl From<&Workspace> for WorkspaceConfig {
|
||||
.workspace_config()
|
||||
.as_ref()
|
||||
.and_then(|c| c.workspace_rules.clone()),
|
||||
work_area_offset: value.work_area_offset(),
|
||||
apply_window_based_work_area_offset: Some(value.apply_window_based_work_area_offset()),
|
||||
window_container_behaviour: *value.window_container_behaviour(),
|
||||
window_container_behaviour_rules: Option::from(window_container_behaviour_rules),
|
||||
float_override: *value.float_override(),
|
||||
tile,
|
||||
layout_flip: value.layout_flip(),
|
||||
floating_layer_behaviour: value.floating_layer_behaviour(),
|
||||
wallpaper: None,
|
||||
@@ -397,7 +412,7 @@ pub enum AppSpecificConfigurationPath {
|
||||
#[serde_with::serde_as]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
/// The `komorebi.json` static configuration file reference for `v0.1.37`
|
||||
/// The `komorebi.json` static configuration file reference for `v0.1.38`
|
||||
pub struct StaticConfig {
|
||||
/// DEPRECATED from v0.1.22: no longer required
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -1331,7 +1346,7 @@ impl StaticConfig {
|
||||
}
|
||||
|
||||
pub fn postload(path: &PathBuf, wm: &Arc<Mutex<WindowManager>>) -> Result<()> {
|
||||
let value = Self::read(path)?;
|
||||
let mut value = Self::read(path)?;
|
||||
let mut wm = wm.lock();
|
||||
|
||||
let configs_with_preference: Vec<_> =
|
||||
@@ -1342,6 +1357,8 @@ impl StaticConfig {
|
||||
workspace_matching_rules.clear();
|
||||
drop(workspace_matching_rules);
|
||||
|
||||
let monitor_count = wm.monitors().len();
|
||||
|
||||
let offset = wm.work_area_offset;
|
||||
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
|
||||
let preferred_config_idx = {
|
||||
@@ -1371,8 +1388,8 @@ impl StaticConfig {
|
||||
});
|
||||
if let Some(monitor_config) = value
|
||||
.monitors
|
||||
.as_ref()
|
||||
.and_then(|ms| idx.and_then(|i| ms.get(i)))
|
||||
.as_mut()
|
||||
.and_then(|ms| idx.and_then(|i| ms.get_mut(i)))
|
||||
{
|
||||
if let Some(used_config_idx) = idx {
|
||||
configs_used.push(used_config_idx);
|
||||
@@ -1395,7 +1412,14 @@ impl StaticConfig {
|
||||
|
||||
monitor.update_workspaces_globals(offset);
|
||||
for (j, ws) in monitor.workspaces_mut().iter_mut().enumerate() {
|
||||
if let Some(workspace_config) = monitor_config.workspaces.get(j) {
|
||||
if let Some(workspace_config) = monitor_config.workspaces.get_mut(j) {
|
||||
if monitor_count > 1
|
||||
&& matches!(workspace_config.layout, Some(DefaultLayout::Scrolling))
|
||||
{
|
||||
tracing::warn!("scrolling layout is only supported for a single monitor; falling back to columns layout");
|
||||
workspace_config.layout = Some(DefaultLayout::Columns);
|
||||
}
|
||||
|
||||
ws.load_static_config(workspace_config)?;
|
||||
}
|
||||
}
|
||||
@@ -1918,7 +1942,7 @@ mod tests {
|
||||
let docs = vec![
|
||||
"0.1.20", "0.1.21", "0.1.22", "0.1.23", "0.1.24", "0.1.25", "0.1.26", "0.1.27",
|
||||
"0.1.28", "0.1.29", "0.1.30", "0.1.31", "0.1.32", "0.1.33", "0.1.34", "0.1.35",
|
||||
"0.1.36",
|
||||
"0.1.36", "0.1.37",
|
||||
];
|
||||
|
||||
let mut versions = vec![];
|
||||
|
||||
@@ -126,7 +126,7 @@ pub struct WindowManager {
|
||||
}
|
||||
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct State {
|
||||
pub monitors: Ring<Monitor>,
|
||||
@@ -329,6 +329,7 @@ impl From<&WindowManager> for State {
|
||||
maximized_window_restore_idx: workspace.maximized_window_restore_idx,
|
||||
floating_windows: workspace.floating_windows.clone(),
|
||||
layout: workspace.layout.clone(),
|
||||
layout_options: workspace.layout_options,
|
||||
layout_rules: workspace.layout_rules.clone(),
|
||||
layout_flip: workspace.layout_flip,
|
||||
workspace_padding: workspace.workspace_padding,
|
||||
@@ -336,6 +337,7 @@ impl From<&WindowManager> for State {
|
||||
latest_layout: workspace.latest_layout.clone(),
|
||||
resize_dimensions: workspace.resize_dimensions.clone(),
|
||||
tile: workspace.tile,
|
||||
work_area_offset: workspace.work_area_offset,
|
||||
apply_window_based_work_area_offset: workspace
|
||||
.apply_window_based_work_area_offset,
|
||||
window_container_behaviour: workspace.window_container_behaviour,
|
||||
@@ -346,7 +348,6 @@ impl From<&WindowManager> for State {
|
||||
layer: workspace.layer,
|
||||
floating_layer_behaviour: workspace.floating_layer_behaviour,
|
||||
globals: workspace.globals,
|
||||
locked_containers: workspace.locked_containers.clone(),
|
||||
wallpaper: workspace.wallpaper.clone(),
|
||||
workspace_config: None,
|
||||
})
|
||||
@@ -821,7 +822,7 @@ impl WindowManager {
|
||||
target_workspace_idx: usize,
|
||||
floating: bool,
|
||||
to_move: &mut Vec<EnforceWorkspaceRuleOp>,
|
||||
) -> () {
|
||||
) {
|
||||
tracing::trace!(
|
||||
"{} should be on monitor {}, workspace {}",
|
||||
window_title,
|
||||
@@ -1579,6 +1580,9 @@ impl WindowManager {
|
||||
workspace.container_padding(),
|
||||
workspace.layout_flip(),
|
||||
&[],
|
||||
workspace.focused_container_idx(),
|
||||
workspace.layout_options(),
|
||||
workspace.latest_layout(),
|
||||
);
|
||||
|
||||
let mut direction = direction;
|
||||
@@ -2956,6 +2960,8 @@ impl WindowManager {
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn stack_all(&mut self) -> Result<()> {
|
||||
self.unstack_all(false)?;
|
||||
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
tracing::info!("stacking all windows on workspace");
|
||||
|
||||
@@ -2982,7 +2988,7 @@ impl WindowManager {
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn unstack_all(&mut self) -> Result<()> {
|
||||
pub fn unstack_all(&mut self, update_workspace: bool) -> Result<()> {
|
||||
self.handle_unmanaged_window_behaviour()?;
|
||||
tracing::info!("unstacking all windows in container");
|
||||
|
||||
@@ -3012,7 +3018,11 @@ impl WindowManager {
|
||||
workspace.focus_container_by_window(hwnd)?;
|
||||
}
|
||||
|
||||
self.update_focused_workspace(self.mouse_follows_focus, true)
|
||||
if update_workspace {
|
||||
self.update_focused_workspace(self.mouse_follows_focus, true)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
@@ -3182,14 +3192,10 @@ impl WindowManager {
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn toggle_lock(&mut self) -> Result<()> {
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
let index = workspace.focused_container_idx();
|
||||
|
||||
if workspace.locked_containers().contains(&index) {
|
||||
workspace.locked_containers_mut().remove(&index);
|
||||
} else {
|
||||
workspace.locked_containers_mut().insert(index);
|
||||
if let Some(container) = workspace.focused_container_mut() {
|
||||
// Toggle the locked flag
|
||||
container.set_locked(!container.locked());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3352,8 +3358,16 @@ impl WindowManager {
|
||||
pub fn change_workspace_layout_default(&mut self, layout: DefaultLayout) -> Result<()> {
|
||||
tracing::info!("changing layout");
|
||||
|
||||
let monitor_count = self.monitors().len();
|
||||
let workspace = self.focused_workspace_mut()?;
|
||||
|
||||
if monitor_count > 1 && matches!(layout, DefaultLayout::Scrolling) {
|
||||
tracing::warn!(
|
||||
"scrolling layout is only supported for a single monitor; not changing layout"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match workspace.layout() {
|
||||
Layout::Default(_) => {}
|
||||
Layout::Custom(layout) => {
|
||||
@@ -4759,6 +4773,44 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_nonexistent_window_from_container() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
|
||||
{
|
||||
// Create a first monitor
|
||||
let mut m = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor1".to_string(),
|
||||
"TestDevice1".to_string(),
|
||||
"TestDeviceID1".to_string(),
|
||||
Some("TestMonitorID1".to_string()),
|
||||
);
|
||||
|
||||
// Create a container
|
||||
let container = Container::default();
|
||||
|
||||
// Should have 3 windows in the container
|
||||
assert_eq!(container.windows().len(), 0);
|
||||
|
||||
// Add the container to a workspace
|
||||
let workspace = m.focused_workspace_mut().unwrap();
|
||||
workspace.add_container_to_back(container);
|
||||
|
||||
// Add monitor to the window manager
|
||||
wm.monitors_mut().push_back(m);
|
||||
}
|
||||
|
||||
// Should receive an error when trying to remove a window from an empty container
|
||||
let result = wm.remove_window_from_container();
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected an error when trying to remove a window from an empty container"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_container_window_in_direction() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
@@ -4828,6 +4880,44 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cycle_nonexistent_windows() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
|
||||
{
|
||||
// Create a first monitor
|
||||
let mut m = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor1".to_string(),
|
||||
"TestDevice1".to_string(),
|
||||
"TestDeviceID1".to_string(),
|
||||
Some("TestMonitorID1".to_string()),
|
||||
);
|
||||
|
||||
// Create a container
|
||||
let container = Container::default();
|
||||
|
||||
// Should have 3 windows in the container
|
||||
assert_eq!(container.windows().len(), 0);
|
||||
|
||||
// Add the container to a workspace
|
||||
let workspace = m.focused_workspace_mut().unwrap();
|
||||
workspace.add_container_to_back(container);
|
||||
|
||||
// Add monitor to the window manager
|
||||
wm.monitors_mut().push_back(m);
|
||||
}
|
||||
|
||||
// Should return an error when trying to cycle through windows in an empty container
|
||||
let result = wm.cycle_container_window_in_direction(CycleDirection::Next);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected an error when cycling through windows in an empty container"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cycle_container_window_index_in_direction() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
@@ -5357,7 +5447,8 @@ mod tests {
|
||||
{
|
||||
// Ensure container 2 is not locked
|
||||
let workspace = wm.focused_workspace_mut().unwrap();
|
||||
assert!(!workspace.locked_containers().contains(&2));
|
||||
assert_eq!(workspace.focused_container_idx(), 2);
|
||||
assert!(!workspace.focused_container().unwrap().locked());
|
||||
}
|
||||
|
||||
// Toggle lock on focused container
|
||||
@@ -5366,7 +5457,7 @@ mod tests {
|
||||
{
|
||||
// Ensure container 2 is locked
|
||||
let workspace = wm.focused_workspace_mut().unwrap();
|
||||
assert!(workspace.locked_containers().contains(&2));
|
||||
assert!(workspace.focused_container().unwrap().locked());
|
||||
}
|
||||
|
||||
// Toggle lock on focused container
|
||||
@@ -5375,7 +5466,7 @@ mod tests {
|
||||
{
|
||||
// Ensure container 2 is not locked
|
||||
let workspace = wm.focused_workspace_mut().unwrap();
|
||||
assert!(!workspace.locked_containers().contains(&2));
|
||||
assert!(!workspace.focused_container().unwrap().locked());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5457,6 +5548,40 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_float_nonexistent_window() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
|
||||
{
|
||||
let mut m = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
// Add another workspace
|
||||
let new_workspace_index = m.new_workspace_idx();
|
||||
m.focus_workspace(new_workspace_index).unwrap();
|
||||
|
||||
// Should have 2 workspaces
|
||||
assert_eq!(m.workspaces().len(), 2);
|
||||
|
||||
// Add monitor to window manager
|
||||
wm.monitors_mut().push_back(m);
|
||||
}
|
||||
|
||||
// Should return an error when trying to float a non-existent window
|
||||
let result = wm.float_window();
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected an error when trying to float a non-existent window"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_maximize_and_unmaximize_window() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
@@ -5603,6 +5728,41 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_maximize_nonexistent_window() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
|
||||
{
|
||||
// Create a monitor
|
||||
let mut m = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
// Create a container
|
||||
let container = Container::default();
|
||||
|
||||
// Add the container to the workspace
|
||||
let workspace = m.focused_workspace_mut().unwrap();
|
||||
workspace.add_container_to_back(container);
|
||||
|
||||
// Add monitor to the window manager
|
||||
wm.monitors_mut().push_back(m);
|
||||
}
|
||||
|
||||
// Should return an error when trying to toggle maximize on a non-existent window
|
||||
let result = wm.toggle_maximize();
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected an error when trying to toggle maximize on a non-existent window"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_monocle_on_and_monocle_off() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
@@ -5674,6 +5834,41 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_monocle_on_and_off_nonexistent_container() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
|
||||
{
|
||||
// Create a monitor
|
||||
let m = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
// Add monitor to the window manager
|
||||
wm.monitors_mut().push_back(m);
|
||||
}
|
||||
|
||||
// Should return an error when trying to move a non-existent container to monocle
|
||||
let result = wm.monocle_on();
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected an error when trying to move a non-existent container to monocle"
|
||||
);
|
||||
|
||||
// Should return an error when trying to restore a non-existent container from monocle
|
||||
let result = wm.monocle_off();
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected an error when trying to restore a non-existent container from monocle"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_monocle() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
@@ -5745,6 +5940,34 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_monocle_nonexistent_container() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
|
||||
{
|
||||
// Create a monitor
|
||||
let m = monitor::new(
|
||||
0,
|
||||
Rect::default(),
|
||||
Rect::default(),
|
||||
"TestMonitor".to_string(),
|
||||
"TestDevice".to_string(),
|
||||
"TestDeviceID".to_string(),
|
||||
Some("TestMonitorID".to_string()),
|
||||
);
|
||||
|
||||
// Add monitor to the window manager
|
||||
wm.monitors_mut().push_back(m);
|
||||
}
|
||||
|
||||
// Should return an error when trying to toggle monocle on a non-existent container
|
||||
let result = wm.toggle_monocle();
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected an error when trying to toggle monocle on a non-existent container"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ensure_named_workspace_for_monitor() {
|
||||
let (mut wm, _context) = setup_window_manager();
|
||||
|
||||
@@ -1180,6 +1180,7 @@ impl WindowsApi {
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn enable_focus_follows_mouse() -> Result<()> {
|
||||
#[allow(clippy::manual_dangling_ptr)]
|
||||
Self::system_parameters_info_w(
|
||||
SPI_SETACTIVEWINDOWTRACKING,
|
||||
0,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::VecDeque;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt::Display;
|
||||
@@ -16,7 +15,8 @@ use crate::core::DefaultLayout;
|
||||
use crate::core::Layout;
|
||||
use crate::core::OperationDirection;
|
||||
use crate::core::Rect;
|
||||
use crate::locked_deque::LockedDeque;
|
||||
use crate::default_layout::LayoutOptions;
|
||||
use crate::lockable_sequence::LockableSequence;
|
||||
use crate::ring::Ring;
|
||||
use crate::should_act;
|
||||
use crate::stackbar_manager;
|
||||
@@ -70,6 +70,8 @@ pub struct Workspace {
|
||||
pub floating_windows: Ring<Window>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
pub layout: Layout,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub layout_options: Option<LayoutOptions>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
pub layout_rules: Vec<(usize, Layout)>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
@@ -85,6 +87,8 @@ pub struct Workspace {
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
pub tile: bool,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub work_area_offset: Option<Rect>,
|
||||
#[getset(get_copy = "pub", set = "pub")]
|
||||
pub apply_window_based_work_area_offset: bool,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
pub window_container_behaviour: Option<WindowContainerBehaviour>,
|
||||
@@ -100,8 +104,6 @@ pub struct Workspace {
|
||||
#[getset(get_copy = "pub", get_mut = "pub", set = "pub")]
|
||||
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
pub locked_containers: BTreeSet<usize>,
|
||||
#[getset(get = "pub", get_mut = "pub", set = "pub")]
|
||||
pub wallpaper: Option<Wallpaper>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[getset(get = "pub", set = "pub")]
|
||||
@@ -139,6 +141,7 @@ impl Default for Workspace {
|
||||
monocle_container_restore_idx: None,
|
||||
floating_windows: Ring::default(),
|
||||
layout: Layout::Default(DefaultLayout::BSP),
|
||||
layout_options: None,
|
||||
layout_rules: vec![],
|
||||
layout_flip: None,
|
||||
workspace_padding: Option::from(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)),
|
||||
@@ -146,6 +149,7 @@ impl Default for Workspace {
|
||||
latest_layout: vec![],
|
||||
resize_dimensions: vec![],
|
||||
tile: true,
|
||||
work_area_offset: None,
|
||||
apply_window_based_work_area_offset: true,
|
||||
window_container_behaviour: None,
|
||||
window_container_behaviour_rules: None,
|
||||
@@ -154,7 +158,6 @@ impl Default for Workspace {
|
||||
floating_layer_behaviour: Default::default(),
|
||||
globals: Default::default(),
|
||||
workspace_config: None,
|
||||
locked_containers: Default::default(),
|
||||
wallpaper: None,
|
||||
}
|
||||
}
|
||||
@@ -205,18 +208,16 @@ impl Workspace {
|
||||
|
||||
if let Some(layout) = &config.layout {
|
||||
self.layout = Layout::Default(*layout);
|
||||
self.tile = true;
|
||||
}
|
||||
|
||||
if let Some(pathbuf) = &config.custom_layout {
|
||||
let layout = CustomLayout::from_path(pathbuf)?;
|
||||
self.layout = Layout::Custom(layout);
|
||||
self.tile = true;
|
||||
}
|
||||
|
||||
if config.custom_layout.is_none() && config.layout.is_none() {
|
||||
self.tile = false;
|
||||
}
|
||||
self.tile =
|
||||
!(config.custom_layout.is_none() && config.layout.is_none() && config.tile.is_none()
|
||||
|| config.tile.is_some_and(|tile| !tile));
|
||||
|
||||
let mut all_layout_rules = vec![];
|
||||
if let Some(layout_rules) = &config.layout_rules {
|
||||
@@ -241,6 +242,8 @@ impl Workspace {
|
||||
self.set_layout_rules(all_layout_rules);
|
||||
}
|
||||
|
||||
self.set_work_area_offset(config.work_area_offset);
|
||||
|
||||
self.set_apply_window_based_work_area_offset(
|
||||
config.apply_window_based_work_area_offset.unwrap_or(true),
|
||||
);
|
||||
@@ -267,6 +270,7 @@ impl Workspace {
|
||||
self.set_layout_flip(config.layout_flip);
|
||||
self.set_floating_layer_behaviour(config.floating_layer_behaviour);
|
||||
self.set_wallpaper(config.wallpaper.clone());
|
||||
self.set_layout_options(config.layout_options);
|
||||
|
||||
self.set_workspace_config(Some(config.clone()));
|
||||
|
||||
@@ -495,7 +499,7 @@ impl Workspace {
|
||||
let border_width = self.globals().border_width;
|
||||
let border_offset = self.globals().border_offset;
|
||||
let work_area = self.globals().work_area;
|
||||
let work_area_offset = self.globals().work_area_offset;
|
||||
let work_area_offset = self.work_area_offset().or(self.globals().work_area_offset);
|
||||
let window_based_work_area_offset = self.globals().window_based_work_area_offset;
|
||||
let window_based_work_area_offset_limit =
|
||||
self.globals().window_based_work_area_offset_limit;
|
||||
@@ -583,6 +587,9 @@ impl Workspace {
|
||||
Some(container_padding),
|
||||
self.layout_flip(),
|
||||
self.resize_dimensions(),
|
||||
self.focused_container_idx(),
|
||||
self.layout_options(),
|
||||
self.latest_layout(),
|
||||
);
|
||||
|
||||
let should_remove_titlebars = REMOVE_TITLEBARS.load(Ordering::SeqCst);
|
||||
@@ -890,10 +897,9 @@ impl Workspace {
|
||||
// this fn respects locked container indexes - we should use it for pretty much everything
|
||||
// except monocle and maximize toggles
|
||||
pub fn insert_container_at_idx(&mut self, idx: usize, container: Container) -> usize {
|
||||
let mut locked_containers = self.locked_containers().clone();
|
||||
let mut ld = LockedDeque::new(self.containers_mut(), &mut locked_containers);
|
||||
let insertion_idx = ld.insert(idx, container);
|
||||
self.locked_containers = locked_containers;
|
||||
let insertion_idx = self
|
||||
.containers_mut()
|
||||
.insert_respecting_locks(idx, container);
|
||||
|
||||
if insertion_idx > self.resize_dimensions().len() {
|
||||
self.resize_dimensions_mut().push(None);
|
||||
@@ -909,10 +915,7 @@ impl Workspace {
|
||||
// this fn respects locked container indexes - we should use it for pretty much everything
|
||||
// except monocle and maximize toggles
|
||||
pub fn remove_container_by_idx(&mut self, idx: usize) -> Option<Container> {
|
||||
let mut locked_containers = self.locked_containers().clone();
|
||||
let mut ld = LockedDeque::new(self.containers_mut(), &mut locked_containers);
|
||||
let container = ld.remove(idx);
|
||||
self.locked_containers = locked_containers;
|
||||
let container = self.containers_mut().remove_respecting_locks(idx);
|
||||
|
||||
if idx < self.resize_dimensions().len() {
|
||||
self.resize_dimensions_mut().remove(idx);
|
||||
@@ -1194,6 +1197,9 @@ impl Workspace {
|
||||
Layout::Default(DefaultLayout::UltrawideVerticalStack) => {
|
||||
self.enforce_resize_for_ultrawide();
|
||||
}
|
||||
Layout::Default(DefaultLayout::Scrolling) => {
|
||||
self.enforce_resize_for_scrolling();
|
||||
}
|
||||
_ => self.enforce_no_resize(),
|
||||
}
|
||||
}
|
||||
@@ -1421,6 +1427,28 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
fn enforce_resize_for_scrolling(&mut self) {
|
||||
let resize_dimensions = self.resize_dimensions_mut();
|
||||
match resize_dimensions.len() {
|
||||
0 | 1 => self.enforce_no_resize(),
|
||||
_ => {
|
||||
let len = resize_dimensions.len();
|
||||
|
||||
for (i, rect) in resize_dimensions.iter_mut().enumerate() {
|
||||
if let Some(rect) = rect {
|
||||
rect.top = 0;
|
||||
rect.bottom = 0;
|
||||
|
||||
if i == 0 {
|
||||
rect.left = 0;
|
||||
} else if i == len - 1 {
|
||||
rect.right = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn enforce_no_resize(&mut self) {
|
||||
for rect in self.resize_dimensions_mut().iter_mut().flatten() {
|
||||
rect.left = 0;
|
||||
@@ -1601,7 +1629,7 @@ impl Workspace {
|
||||
}
|
||||
|
||||
pub fn swap_containers(&mut self, i: usize, j: usize) {
|
||||
self.containers.swap(i, j);
|
||||
self.containers.elements_mut().swap_respecting_locks(i, j);
|
||||
self.focus_container(j);
|
||||
}
|
||||
|
||||
@@ -1700,7 +1728,6 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::container::Container;
|
||||
use crate::Window;
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
@@ -1708,20 +1735,18 @@ mod tests {
|
||||
let mut ws = Workspace::default();
|
||||
|
||||
let mut state = HashMap::new();
|
||||
let mut locked = BTreeSet::new();
|
||||
|
||||
// add 3 containers
|
||||
// add 4 containers
|
||||
for i in 0..4 {
|
||||
let container = Container::default();
|
||||
let mut container = Container::default();
|
||||
if i == 3 {
|
||||
container.set_locked(true); // set index 3 locked
|
||||
}
|
||||
state.insert(i, container.id().to_string());
|
||||
ws.add_container_to_back(container);
|
||||
}
|
||||
assert_eq!(ws.containers().len(), 4);
|
||||
|
||||
// set index 3 locked
|
||||
locked.insert(3);
|
||||
ws.locked_containers = locked;
|
||||
|
||||
// focus container at index 2
|
||||
ws.focus_container(2);
|
||||
|
||||
@@ -1753,20 +1778,17 @@ mod tests {
|
||||
fn test_locked_containers_remove_window() {
|
||||
let mut ws = Workspace::default();
|
||||
|
||||
let mut locked = BTreeSet::new();
|
||||
|
||||
// add 4 containers
|
||||
for i in 0..4 {
|
||||
let mut container = Container::default();
|
||||
container.windows_mut().push_back(Window::from(i));
|
||||
if i == 1 {
|
||||
container.set_locked(true);
|
||||
}
|
||||
ws.add_container_to_back(container);
|
||||
}
|
||||
assert_eq!(ws.containers().len(), 4);
|
||||
|
||||
// set index 1 locked
|
||||
locked.insert(1);
|
||||
ws.locked_containers = locked;
|
||||
|
||||
ws.remove_window(0).unwrap();
|
||||
assert_eq!(ws.containers()[0].focused_window().unwrap().hwnd, 2);
|
||||
// index 1 should still be the same
|
||||
@@ -1778,20 +1800,17 @@ mod tests {
|
||||
fn test_locked_containers_toggle_float() {
|
||||
let mut ws = Workspace::default();
|
||||
|
||||
let mut locked = BTreeSet::new();
|
||||
|
||||
// add 4 containers
|
||||
for i in 0..4 {
|
||||
let mut container = Container::default();
|
||||
container.windows_mut().push_back(Window::from(i));
|
||||
if i == 1 {
|
||||
container.set_locked(true);
|
||||
}
|
||||
ws.add_container_to_back(container);
|
||||
}
|
||||
assert_eq!(ws.containers().len(), 4);
|
||||
|
||||
// set index 1 locked
|
||||
locked.insert(1);
|
||||
ws.locked_containers = locked;
|
||||
|
||||
// set index 0 focused
|
||||
ws.focus_container(0);
|
||||
|
||||
@@ -1823,20 +1842,17 @@ mod tests {
|
||||
fn test_locked_containers_stack() {
|
||||
let mut ws = Workspace::default();
|
||||
|
||||
let mut locked = BTreeSet::new();
|
||||
|
||||
// add 6 containers
|
||||
for i in 0..6 {
|
||||
let mut container = Container::default();
|
||||
container.windows_mut().push_back(Window::from(i));
|
||||
if i == 4 {
|
||||
container.set_locked(true);
|
||||
}
|
||||
ws.add_container_to_back(container);
|
||||
}
|
||||
assert_eq!(ws.containers().len(), 6);
|
||||
|
||||
// set index 4 locked
|
||||
locked.insert(4);
|
||||
ws.locked_containers = locked;
|
||||
|
||||
// set index 3 focused
|
||||
ws.focus_container(3);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebic-no-console"
|
||||
version = "0.1.37"
|
||||
version = "0.1.38"
|
||||
description = "The command-line interface (without a console) for Komorebi, a tiling window manager for Windows"
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2021"
|
||||
|
||||
@@ -184,6 +184,10 @@ MonitorWorkAreaOffset(monitor, left, top, right, bottom) {
|
||||
RunWait("komorebic.exe monitor-work-area-offset " monitor " " left " " top " " right " " bottom, , "Hide")
|
||||
}
|
||||
|
||||
WorkspaceWorkAreaOffset(monitor, workspace, left, top, right, bottom) {
|
||||
RunWait("komorebic.exe workspace-work-area-offset " monitor " "workspace" " left " " top " " right " " bottom, , "Hide")
|
||||
}
|
||||
|
||||
AdjustContainerPadding(sizing, adjustment) {
|
||||
RunWait("komorebic.exe adjust-container-padding " sizing " " adjustment, , "Hide")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "komorebic"
|
||||
version = "0.1.37"
|
||||
version = "0.1.38"
|
||||
description = "The command-line interface for Komorebi, a tiling window manager for Windows"
|
||||
repository = "https://github.com/LGUG2Z/komorebi"
|
||||
edition = "2021"
|
||||
|
||||
@@ -6,9 +6,11 @@ use komorebi_client::replace_env_in_path;
|
||||
use komorebi_client::PathExt;
|
||||
use std::fs::File;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io;
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::Write;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
@@ -91,8 +93,7 @@ lazy_static! {
|
||||
|
||||
assert!(
|
||||
whkd_config_home.is_dir(),
|
||||
"$Env:WHKD_CONFIG_HOME is set to '{}', which is not a valid directory",
|
||||
home_path
|
||||
"$Env:WHKD_CONFIG_HOME is set to '{home_path}', which is not a valid directory"
|
||||
);
|
||||
|
||||
whkd_config_home
|
||||
@@ -428,6 +429,22 @@ struct MonitorWorkAreaOffset {
|
||||
bottom: i32,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct WorkspaceWorkAreaOffset {
|
||||
/// Monitor index (zero-indexed)
|
||||
monitor: usize,
|
||||
/// Workspace index (zero-indexed)
|
||||
workspace: usize,
|
||||
/// Size of the left work area offset (set right to left * 2 to maintain right padding)
|
||||
left: i32,
|
||||
/// Size of the top work area offset (set bottom to the same value to maintain bottom padding)
|
||||
top: i32,
|
||||
/// Size of the right work area offset
|
||||
right: i32,
|
||||
/// Size of the bottom work area offset
|
||||
bottom: i32,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct MonitorIndexPreference {
|
||||
/// Preferred monitor index (zero-indexed)
|
||||
@@ -963,6 +980,12 @@ struct EagerFocus {
|
||||
exe: String,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct ScrollingLayoutColumns {
|
||||
/// Desired number of visible columns
|
||||
count: NonZeroUsize,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, about, version = build::CLAP_LONG_VERSION)]
|
||||
struct Opts {
|
||||
@@ -1182,6 +1205,9 @@ enum SubCommand {
|
||||
/// Set offsets for a monitor to exclude parts of the work area from tiling
|
||||
#[clap(arg_required_else_help = true)]
|
||||
MonitorWorkAreaOffset(MonitorWorkAreaOffset),
|
||||
/// Set offsets for a workspace to exclude parts of the work area from tiling
|
||||
#[clap(arg_required_else_help = true)]
|
||||
WorkspaceWorkAreaOffset(WorkspaceWorkAreaOffset),
|
||||
/// Toggle application of the window-based work area offset for the focused workspace
|
||||
ToggleWindowBasedWorkAreaOffset,
|
||||
/// Set container padding on the focused workspace
|
||||
@@ -1202,6 +1228,9 @@ enum SubCommand {
|
||||
/// Cycle between available layouts
|
||||
#[clap(arg_required_else_help = true)]
|
||||
CycleLayout(CycleLayout),
|
||||
/// Set the number of visible columns for the Scrolling layout on the focused workspace
|
||||
#[clap(arg_required_else_help = true)]
|
||||
ScrollingLayoutColumns(ScrollingLayoutColumns),
|
||||
/// Load a custom layout from file for the focused workspace
|
||||
#[clap(hide = true)]
|
||||
#[clap(arg_required_else_help = true)]
|
||||
@@ -1298,7 +1327,7 @@ enum SubCommand {
|
||||
ToggleWorkspaceFloatOverride,
|
||||
/// Toggle between the Tiling and Floating layers on the focused workspace
|
||||
ToggleWorkspaceLayer,
|
||||
/// Toggle window tiling on the focused workspace
|
||||
/// Toggle the paused state for all window tiling
|
||||
TogglePause,
|
||||
/// Toggle window tiling on the focused workspace
|
||||
ToggleTiling,
|
||||
@@ -1552,6 +1581,33 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
SubCommand::Quickstart => {
|
||||
fn write_file_with_prompt(
|
||||
path: &PathBuf,
|
||||
content: &str,
|
||||
created_files: &mut Vec<String>,
|
||||
) -> Result<()> {
|
||||
if path.exists() {
|
||||
print!(
|
||||
"{} will be overwritten, do you want to continue? (y/N): ",
|
||||
path.display()
|
||||
);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim().to_lowercase();
|
||||
if trimmed == "y" || trimmed == "yes" {
|
||||
std::fs::write(path, content)?;
|
||||
created_files.push(path.display().to_string());
|
||||
} else {
|
||||
println!("Skipping {}", path.display());
|
||||
}
|
||||
} else {
|
||||
std::fs::write(path, content)?;
|
||||
created_files.push(path.display().to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let local_appdata_dir = data_local_dir().expect("could not find localdata dir");
|
||||
let data_dir = local_appdata_dir.join("komorebi");
|
||||
std::fs::create_dir_all(&*WHKD_CONFIG_DIR)?;
|
||||
@@ -1567,17 +1623,30 @@ fn main() -> Result<()> {
|
||||
komorebi_json.replace("Env:USERPROFILE", "Env:KOMOREBI_CONFIG_HOME");
|
||||
}
|
||||
|
||||
std::fs::write(HOME_DIR.join("komorebi.json"), komorebi_json)?;
|
||||
std::fs::write(HOME_DIR.join("komorebi.bar.json"), komorebi_bar_json)?;
|
||||
let komorebi_path = HOME_DIR.join("komorebi.json");
|
||||
let bar_path = HOME_DIR.join("komorebi.bar.json");
|
||||
let applications_path = HOME_DIR.join("applications.json");
|
||||
let whkdrc_path = WHKD_CONFIG_DIR.join("whkdrc");
|
||||
|
||||
let mut written_files = Vec::new();
|
||||
|
||||
write_file_with_prompt(&komorebi_path, &komorebi_json, &mut written_files)?;
|
||||
write_file_with_prompt(&bar_path, &komorebi_bar_json, &mut written_files)?;
|
||||
|
||||
let applications_json = include_str!("../applications.json");
|
||||
std::fs::write(HOME_DIR.join("applications.json"), applications_json)?;
|
||||
write_file_with_prompt(&applications_path, applications_json, &mut written_files)?;
|
||||
|
||||
let whkdrc = include_str!("../../docs/whkdrc.sample");
|
||||
std::fs::write(WHKD_CONFIG_DIR.join("whkdrc"), whkdrc)?;
|
||||
|
||||
println!("Example komorebi.json, komorebi.bar.json, whkdrc and latest applications.json files created");
|
||||
println!("You can now run komorebic start --whkd --bar");
|
||||
write_file_with_prompt(&whkdrc_path, whkdrc, &mut written_files)?;
|
||||
if written_files.is_empty() {
|
||||
println!("\nNo files were written.")
|
||||
} else {
|
||||
println!(
|
||||
"\nThe following example files were written:\n{}",
|
||||
written_files.join("\n")
|
||||
);
|
||||
}
|
||||
println!("\nYou can now run komorebic start --whkd --bar");
|
||||
}
|
||||
SubCommand::EnableAutostart(args) => {
|
||||
let mut current_exe = std::env::current_exe().expect("unable to get exec path");
|
||||
@@ -1929,6 +1998,20 @@ fn main() -> Result<()> {
|
||||
bottom: arg.bottom,
|
||||
}))?;
|
||||
}
|
||||
|
||||
SubCommand::WorkspaceWorkAreaOffset(arg) => {
|
||||
send_message(&SocketMessage::WorkspaceWorkAreaOffset(
|
||||
arg.monitor,
|
||||
arg.workspace,
|
||||
Rect {
|
||||
left: arg.left,
|
||||
top: arg.top,
|
||||
right: arg.right,
|
||||
bottom: arg.bottom,
|
||||
},
|
||||
))?;
|
||||
}
|
||||
|
||||
SubCommand::ToggleWindowBasedWorkAreaOffset => {
|
||||
send_message(&SocketMessage::ToggleWindowBasedWorkAreaOffset)?;
|
||||
}
|
||||
@@ -2625,6 +2708,9 @@ if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) {
|
||||
SubCommand::CycleLayout(arg) => {
|
||||
send_message(&SocketMessage::CycleLayout(arg.cycle_direction))?;
|
||||
}
|
||||
SubCommand::ScrollingLayoutColumns(arg) => {
|
||||
send_message(&SocketMessage::ScrollingLayoutColumns(arg.count))?;
|
||||
}
|
||||
SubCommand::LoadCustomLayout(arg) => {
|
||||
send_message(&SocketMessage::ChangeLayoutCustom(arg.path))?;
|
||||
}
|
||||
|
||||
@@ -158,6 +158,7 @@ nav:
|
||||
- cli/invisible-borders.md
|
||||
- cli/global-work-area-offset.md
|
||||
- cli/monitor-work-area-offset.md
|
||||
- cli/workspace-work-area-offset.md
|
||||
- cli/toggle-window-based-work-area-offset.md
|
||||
- cli/focused-workspace-container-padding.md
|
||||
- cli/focused-workspace-padding.md
|
||||
@@ -165,6 +166,7 @@ nav:
|
||||
- cli/adjust-workspace-padding.md
|
||||
- cli/change-layout.md
|
||||
- cli/cycle-layout.md
|
||||
- cli/scrolling-layout-columns.md
|
||||
- cli/flip-layout.md
|
||||
- cli/promote.md
|
||||
- cli/promote-focus.md
|
||||
@@ -253,4 +255,4 @@ nav:
|
||||
- cli/static-config-schema.md
|
||||
- cli/generate-static-config.md
|
||||
- cli/enable-autostart.md
|
||||
- cli/disable-autostart.md
|
||||
- cli/disable-autostart.md
|
||||
|
||||
104021
schema.bar.json
104021
schema.bar.json
File diff suppressed because it is too large
Load Diff
65
schema.json
65
schema.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "StaticConfig",
|
||||
"description": "The `komorebi.json` static configuration file reference for `v0.1.37`",
|
||||
"description": "The `komorebi.json` static configuration file reference for `v0.1.38`",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"animation": {
|
||||
@@ -1790,7 +1790,8 @@
|
||||
"HorizontalStack",
|
||||
"UltrawideVerticalStack",
|
||||
"Grid",
|
||||
"RightMainVerticalStack"
|
||||
"RightMainVerticalStack",
|
||||
"Scrolling"
|
||||
]
|
||||
},
|
||||
"layout_flip": {
|
||||
@@ -1802,6 +1803,27 @@
|
||||
"HorizontalAndVertical"
|
||||
]
|
||||
},
|
||||
"layout_options": {
|
||||
"description": "Layout-specific options (default: None)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"scrolling": {
|
||||
"description": "Options related to the Scrolling layout",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"columns"
|
||||
],
|
||||
"properties": {
|
||||
"columns": {
|
||||
"description": "Desired number of visible columns (default: 3)",
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"layout_rules": {
|
||||
"description": "Layout rules in the format of threshold => layout (default: None)",
|
||||
"type": "object",
|
||||
@@ -1815,7 +1837,8 @@
|
||||
"HorizontalStack",
|
||||
"UltrawideVerticalStack",
|
||||
"Grid",
|
||||
"RightMainVerticalStack"
|
||||
"RightMainVerticalStack",
|
||||
"Scrolling"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -1823,6 +1846,10 @@
|
||||
"description": "Name",
|
||||
"type": "string"
|
||||
},
|
||||
"tile": {
|
||||
"description": "Enable or disable tiling for the workspace (default: true)",
|
||||
"type": "boolean"
|
||||
},
|
||||
"wallpaper": {
|
||||
"description": "Specify a wallpaper for this workspace",
|
||||
"type": "object",
|
||||
@@ -2115,6 +2142,38 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"work_area_offset": {
|
||||
"description": "Workspace specific work area offset (default: None)",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"bottom",
|
||||
"left",
|
||||
"right",
|
||||
"top"
|
||||
],
|
||||
"properties": {
|
||||
"bottom": {
|
||||
"description": "The bottom point in a Win32 Rect",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"left": {
|
||||
"description": "The left point in a Win32 Rect",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"right": {
|
||||
"description": "The right point in a Win32 Rect",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"top": {
|
||||
"description": "The top point in a Win32 Rect",
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
},
|
||||
"workspace_padding": {
|
||||
"description": "Workspace padding (default: global)",
|
||||
"type": "integer",
|
||||
|
||||
Reference in New Issue
Block a user