Compare commits

...

116 Commits

Author SHA1 Message Date
LGUG2Z
8ef1bcf26e chore(release): v0.1.36 2025-04-12 10:52:31 -07:00
LGUG2Z
d146f35c25 chore(deps): cargo update 2025-04-12 10:52:31 -07:00
dependabot[bot]
fc07ba3dd9 chore(deps): bump crossbeam-channel from 0.5.14 to 0.5.15
Bumps [crossbeam-channel](https://github.com/crossbeam-rs/crossbeam) from 0.5.14 to 0.5.15.
- [Release notes](https://github.com/crossbeam-rs/crossbeam/releases)
- [Changelog](https://github.com/crossbeam-rs/crossbeam/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crossbeam-rs/crossbeam/compare/crossbeam-channel-0.5.14...crossbeam-channel-0.5.15)

---
updated-dependencies:
- dependency-name: crossbeam-channel
  dependency-version: 0.5.15
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-10 09:54:12 -07:00
LGUG2Z
3a8a61119d fix(config): update monitor wallpaper on reload 2025-04-09 16:09:17 -07:00
LGUG2Z
d24beb60b1 feat(config): add per-monitor floating layer behaviour opt
This commit adds an option to set the floating layer behaviour for all
workspaces on a monitor.

Overrides set on an individual workspace level will take precedence over
the option set at the monitor level.
2025-04-09 09:42:46 -07:00
Jerry Kingsbury
7daf3242e2 test(wm): add float and lock toggle tests
Created a test for the toggle_lock function. The test creates a
workspace with multiple containers and checks to see whether the
container was added or removed from the locked containers list when
calling the toggle_lock function.

Created a test for the float_window function. The test checks to ensure
that when calling the float_window function, the window is added to the
floating_windows list and removed from the containers window list. The
test will also check the count and ensure the expected window was added.
2025-04-09 08:55:10 -07:00
LGUG2Z
b6e261aef6 fix(bar): avoid retile messages on ws switch
This commit removes RetileWithResizeDimensions messages from the batches
that are sent to komorebi when changing workspace.

I'm not really sure why this was added in the first place, but removing
it doesn't seem to impact layouts on workspace switch, and more
important, by removing this message I'm no longer able to reproduce the
sudden exits of komorebi.exe under sustained workspace switching calls
made via the bar.
2025-04-08 17:20:02 -07:00
LGUG2Z
d40c304324 fix(borders): avoid deadlock on state hashmaps
This commit addresses a deadlock on WINDOW_BORDERS which can occur under
load, particularly when switching workspaces.

When switching workspaces using the komorebi-bar,
RetileWithResizeDimensions is also called, which calls
border_manager::destroy_all_borders.

At the same time, border_manager::window_border is called when hiding
borders from the previous workspace. Both of these functions try to take
locks on WINDOWS_BORDERS and BORDER_STATE.

border_manager::window_border is called in a new thread by hide_border,
so if this has not finished with the lock by the time
destroy_all_borders requests it, we get stuck in a deadlock.

The changes in this commit ensure that the BORDER_STATE lock is dropped
as early as possible in destroy_all_borders, and that we are not holding
the WINDOWS_BORDERS lock in window_border while trying to get the
BORDER_STATE lock to look up BorderInfo.

The logs which show the initial deadlock being detected:

2025-04-08T22:49:37.641888Z  WARN komorebi::process_command: could not acquire window manager lock, not processing message: FocusWorkspaceNumber
2025-04-08T22:49:38.294952Z  WARN komorebi::process_command: could not acquire window manager lock, not processing message: FocusWorkspaceNumber
2025-04-08T22:49:39.225645Z ERROR komorebi: 1 deadlocks detected
2025-04-08T22:49:39.225826Z ERROR komorebi: deadlock #0
2025-04-08T22:49:39.225950Z ERROR komorebi: thread id: 9896
2025-04-08T22:49:39.226072Z ERROR komorebi:    0:     0x7ff68fd33dec - backtrace::backtrace::dbghelp64::trace
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\backtrace-0.3.71\src\backtrace\dbghelp64.rs:99
                           backtrace::backtrace::trace_unsynchronized<backtrace::capture::impl$1::create::closure_env$0>
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\backtrace-0.3.71\src\backtrace\mod.rs:66
   1:     0x7ff68fd33c87 - backtrace::backtrace::trace<backtrace::capture::impl$1::create::closure_env$0>
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\backtrace-0.3.71\src\backtrace\mod.rs:53
   2:     0x7ff68fd3c03e - backtrace::capture::Backtrace::create
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\backtrace-0.3.71\src\capture.rs:193
   3:     0x7ff68fd3bfae - backtrace::capture::Backtrace::new
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\backtrace-0.3.71\src\capture.rs:158
   4:     0x7ff68f937b54 - parking_lot_core::parking_lot::deadlock_impl::on_unpark
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot_core-0.9.10\src\parking_lot.rs:1211
   5:     0x7ff68f921f0e - parking_lot_core::parking_lot::deadlock::on_unpark
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot_core-0.9.10\src\parking_lot.rs:1144
   6:     0x7ff68f92bf88 - parking_lot_core::parking_lot::park::closure$0<parking_lot::raw_mutex::impl$3::lock_slow::closure_env$0,parking_lot::raw_mutex::impl$3::lock_slow::closure_env$1,parking_lot::raw_mutex::impl$3::lock_slow::closure_env$2>
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot_core-0.9.10\src\parking_lot.rs:637
   7:     0x7ff68f92a777 - parking_lot_core::parking_lot::with_thread_data
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot_core-0.9.10\src\parking_lot.rs:207
                           parking_lot_core::parking_lot::park<parking_lot::raw_mutex::impl$3::lock_slow::closure_env$0,parking_lot::raw_mutex::impl$3::lock_slow::closure_env$1,parking_lot::raw_mutex::impl$3::lock_slow::closure_env$2>
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot_core-0.9.10\src\parking_lot.rs:600
   8:     0x7ff68f926fa0 - parking_lot::raw_mutex::RawMutex::lock_slow
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot-0.12.3\src\raw_mutex.rs:262
   9:     0x7ff68f1daac6 - parking_lot::raw_mutex::impl$0::lock
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot-0.12.3\src\raw_mutex.rs:72
  10:     0x7ff68ef05eb3 - lock_api::mutex::Mutex<parking_lot::raw_mutex::RawMutex,std::collections::hash::map::HashMap<isize,alloc::string::String,std::hash::random::RandomState> >::lock<parking_lot::raw_mutex::RawMutex,std::collections::hash::map::HashMap<isize,alloc::string::Str
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\lock_api-0.4.12\src\mutex.rs:223
  11:     0x7ff68ef0713d - komorebi::border_manager::destroy_all_borders
                               at komorebi\src\border_manager\mod.rs:139
  12:     0x7ff68f194e61 - komorebi::window_manager::WindowManager::process_command<ref_mut$<uds_windows::stdnet::net::UnixStream> >
                               at komorebi\src\process_command.rs:918
  13:     0x7ff68ef1fdcf - komorebi::process_command::read_commands_uds
                               at komorebi\src\process_command.rs:2292
  14:     0x7ff68f231eb5 - komorebi::process_command::listen_for_commands::closure$0::closure$0::closure$0
                               at komorebi\src\process_command.rs:123
  15:     0x7ff68f32bcf3 - core::hint::black_box
                               at C:\Users\LGUG2Z\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\hint.rs:477
                           std::sys::backtrace::__rust_begin_short_backtrace<komorebi::process_command::listen_for_commands::closure$0::closure$0::closure_env$0,tuple$<> >
                               at C:\Users\LGUG2Z\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\sys\backtrace.rs:152
  16:     0x7ff68f00d1b4 - std::thread::impl$0::spawn_unchecked_::closure$1::closure$0<komorebi::process_command::listen_for_commands::closure$0::closure$0::closure_env$0,tuple$<> >
                               at C:\Users\LGUG2Z\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\thread\mod.rs:559
  17:     0x7ff68f356ea1 - core::panic::unwind_safe::impl$25::call_once<tuple$<>,std::thread::impl$0::spawn_unchecked_::closure$1::closure_env$0<komorebi::process_command::listen_for_commands::closure$0::closure$0::closure_env$0,tuple$<> > >
                               at C:\Users\LGUG2Z\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\panic\unwind_safe.rs:272
  18:     0x7ff68f1d1e00 - std::panicking::try::do_call<core::panic::unwind_safe::AssertUnwindSafe<std::thread::impl$0::spawn_unchecked_::closure$1::closure_env$0<komorebi::process_command::listen_for_commands::closure$0::closure$0::closure_env$0,tuple$<> > >,tuple$<> >
                               at C:\Users\LGUG2Z\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\panicking.rs:587
  19:     0x7ff68f0102b3 - std::thread::impl$7::drop::closure$0<enum2$<core::result::Result<tuple$<>,eyre::Report> > >
  20:     0x7ff68f007be7 - std::panicking::try
                               at C:\Users\LGUG2Z\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\panicking.rs:550
                           std::panic::catch_unwind
                               at C:\Users\LGUG2Z\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\panic.rs:358
                           std::thread::impl$0::spawn_unchecked_::closure$1<komorebi::process_command::listen_for_commands::closure$0::closure$0::closure_env$0,tuple$<> >
                               at C:\Users\LGUG2Z\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\std\src\thread\mod.rs:557
  21:     0x7ff68ecfeb8e - core::ops::function::FnOnce::call_once<std::thread::impl$0::spawn_unchecked_::closure_env$1<komorebi::process_command::listen_for_commands::closure$0::closure$0::closure_env$0,tuple$<> >,tuple$<> >
                               at C:\Users\LGUG2Z\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\ops\function.rs:250
  22:     0x7ff68fe84f1d - alloc::boxed::impl$28::call_once
                               at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library\alloc\src\boxed.rs:1976
                           alloc::boxed::impl$28::call_once
                               at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library\alloc\src\boxed.rs:1976
                           std::sys::pal::windows::thread::impl$0::new::thread_start
                               at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library\std\src\sys\pal\windows\thread.rs:56
  23:     0x7ff8ef27257d - BaseThreadInitThunk
  24:     0x7ff8f042af28 - RtlUserThreadStart

2025-04-08T22:49:39.228617Z ERROR komorebi: thread id: 62364
2025-04-08T22:49:39.228731Z ERROR komorebi:    0:     0x7ff68fd33dec - backtrace::backtrace::dbghelp64::trace
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\backtrace-0.3.71\src\backtrace\dbghelp64.rs:99
                           backtrace::backtrace::trace_unsynchronized<backtrace::capture::impl$1::create::closure_env$0>
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\backtrace-0.3.71\src\backtrace\mod.rs:66
   1:     0x7ff68fd33c87 - backtrace::backtrace::trace<backtrace::capture::impl$1::create::closure_env$0>
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\backtrace-0.3.71\src\backtrace\mod.rs:53
   2:     0x7ff68fd3c03e - backtrace::capture::Backtrace::create
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\backtrace-0.3.71\src\capture.rs:193
   3:     0x7ff68fd3bfae - backtrace::capture::Backtrace::new
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\backtrace-0.3.71\src\capture.rs:158
   4:     0x7ff68f937b54 - parking_lot_core::parking_lot::deadlock_impl::on_unpark
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot_core-0.9.10\src\parking_lot.rs:1211
   5:     0x7ff68f921f0e - parking_lot_core::parking_lot::deadlock::on_unpark
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot_core-0.9.10\src\parking_lot.rs:1144
   6:     0x7ff68f92bf88 - parking_lot_core::parking_lot::park::closure$0<parking_lot::raw_mutex::impl$3::lock_slow::closure_env$0,parking_lot::raw_mutex::impl$3::lock_slow::closure_env$1,parking_lot::raw_mutex::impl$3::lock_slow::closure_env$2>
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot_core-0.9.10\src\parking_lot.rs:637
   7:     0x7ff68f92a777 - parking_lot_core::parking_lot::with_thread_data
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot_core-0.9.10\src\parking_lot.rs:207
                           parking_lot_core::parking_lot::park<parking_lot::raw_mutex::impl$3::lock_slow::closure_env$0,parking_lot::raw_mutex::impl$3::lock_slow::closure_env$1,parking_lot::raw_mutex::impl$3::lock_slow::closure_env$2>
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot_core-0.9.10\src\parking_lot.rs:600
   8:     0x7ff68f926fa0 - parking_lot::raw_mutex::RawMutex::lock_slow
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot-0.12.3\src\raw_mutex.rs:262
   9:     0x7ff68f1daac6 - parking_lot::raw_mutex::impl$0::lock
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\parking_lot-0.12.3\src\raw_mutex.rs:72
  10:     0x7ff68ef05d93 - lock_api::mutex::Mutex<parking_lot::raw_mutex::RawMutex,std::collections::hash::map::HashMap<alloc::string::String,alloc::boxed::Box<komorebi::border_manager::border::Border,alloc::alloc::Global>,std::hash::random::RandomState> >::lock<parking_lot::raw_mu
                               at C:\Users\LGUG2Z\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\lock_api-0.4.12\src\mutex.rs:223
  11:     0x7ff68f2064de - komorebi::border_manager::window_border::closure$0
                               at komorebi\src\border_manager\mod.rs:109
  12:     0x7ff68eddeca9 - enum2$<core::option::Option<ref$<alloc::string::String> > >::and_then<ref$<alloc::string::String>,komorebi::border_manager::BorderInfo,komorebi::border_manager::window_border::closure_env$0>
                               at C:\Users\LGUG2Z\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\option.rs:1452
  13:     0x7ff68ef06339 - komorebi::border_manager::window_border
                               at komorebi\src\border_manager\mod.rs:108
2025-04-08 16:14:41 -07:00
LGUG2Z
69d252ba12 feat(wm): drop empty containers on ws update
We shouldn't ever have empty containers, but never say never because
someone on the Discord has an empty container with no Windows that
continues to take up a tile. This commit adds a call to drop all
containers without any windows whenever Workspace::update is called.
2025-04-07 15:28:29 -07:00
LGUG2Z
2ac477d89f docs(mkdocs): reduce number of tabs to improve nav 2025-04-07 11:02:57 -07:00
alex-ds13
6db73151f7 fix(wm): remove border width/offset when disabled
This commit makes sure the border's width and offset is removed when you
disable the borders.

If you want to still have that size on your gaps when borders are
disabled then you should add it to the `default_container_padding` or to
the per-monitor or per-workspace `container_padding`.
2025-04-07 10:12:18 -07:00
alex-ds13
2d6ff0708f fix(docs): borders are enabled by default 2025-04-07 10:12:18 -07:00
Csaba
13a519fb29 fix(cli): restart bar on replace-configuration
This commit ensures that the replace-configuration command also replaces
bars.

Already running bars are stopped and new bars are started using the new
configuration.
2025-04-06 21:19:41 -07:00
amrbashir
9f8e4b9dca feat(core): use PathExt to unify env var resolution
This new implementation allows for expanding any environment variable so
it is not limited to just `~`, `$HOME`, `$Env:USERPROFILE` and
`$Env:KOMOREBI_CONFIG_HOME`.

It expands the follwing formats:
- CMD: `%variable%`
- PowerShell: `$Env:variable`
- Bash: `$variable`

I searched throughout the code base for path and migrate any code that
might need to PathExt::replace_env.

It is possible that I might have missed a few places due to my
unfamiliarity with the code base, so if you find any, please let me
know.

Most of the paths that needed this trait, are in:

- Clap arguments, and that was handled by #[value_parse] attribute and a
  helper function.
- SocketMessage and that was handled by custom deserialization with the
  help of serde_with crate
2025-04-06 21:17:35 -07:00
Jerry Kingsbury
5a0196ac9d test(monitor.rs): ensure workspace count test
Created tests for the ensure_workspace_count function. The function is
tested by calling the function with only the default workspace, after
creating a workspace, and after we already have at least the number of
monitors passed into the funtion.
2025-04-05 12:14:10 -07:00
Jerry Kingsbury
46d0e340f9 test(monitor.rs): move container to workspace test
Created a test for the move_container_to_workspace funtion. The test
creates a workspace with 3 containers and an empty workspace, and
ensuresthat the container is moved to the correct workspace and that if
the workspace focus has changed when setting follow focus to true.
2025-04-05 12:14:10 -07:00
Jerry Kingsbury
371ef88ecb test(workspace): visible windows test
Created a test for the visible_windows function. The test creates two
windows in a workspace and checks to see if the the first created window
is the visible window. The test then maximizes the second window and
checks to ensure both windows are visible.

Expanded visible window test to ensure that a visible window will
display when adding another container.
2025-04-05 12:14:02 -07:00
dependabot[bot]
f5b5070436 chore(deps): bump openssl from 0.10.71 to 0.10.72
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.71 to 0.10.72.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.71...openssl-v0.10.72)

---
updated-dependencies:
- dependency-name: openssl
  dependency-version: 0.10.72
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-04 16:29:12 -07:00
LGUG2Z
c8320552b0 feat(cli): add session float rule cmds
This commit introduces three new commands, session-float-rule,
session-float-rules, and clear-session-float-rules, which add a
composite float rule for the currently focused window for the duration
of the komorebi session, print the float rules scoped to the current
komorebi session, and clear any float rules scoped to the current
komorebi session respectively.

The composite rule created is fairly strict, using
MatchingStrategy::Equals on the Exe, Class and Title.

Users can run session-float-rule as they are working to avoid having to
break their workflow and edit their configuration file, and when they
are ready, they can run session-float-rules to print out the composite
rules which have been generated and added to the current session to
further refine before adding them to their configuration files.

re #1402
2025-04-04 11:56:25 -07:00
alex-ds13
2a5a960c34 feat(wm): allow setting wallpaper per monitor
This commit adds the option to set `Wallpaper` per monitor. When
changing workspaces it will first check for a workspace wallpaper, if
there is none it then checks for a monitor wallpaper.
2025-04-04 11:12:47 -07:00
alex-ds13
10ab43a8ae fix(wm): properly update border colors
This commit changes the border manager notification to an enum that can
be a `Notification::Update(Option<isize>)` which should work like before
or a `Notification::ForceUpdate` which will always update the borders
and it will also update the border's brushes. To do so, this commit
moved the brush creation from the `create()` function to a new
`update_brushes` function on the `Border` itself and it changed the
`Border` `render_target` from a `OnceLock` to an `Option<RenderTarget>`
so that we can always change it when we need to.
This commit also adds a new function called `send_force_update()` to the
border manager to make it easier to send this notification.

This commit also added a check for `force_update_borders` on the
`process_command` function so that any command that reloads the
configuration, changes the theme or changes any border config can set it
to `true`. When this is true we send the `ForceUpdate` notification, if
false we send the normal `Update` notification as before.

The theme manager now always sends the `ForceUpdate` notification to the
border manager.

This commit adds a new function to the window manager called
`apply_wallpaper_for_monitor_workspace` which in turn calls the
`apply_wallpaper` function for the workspace specified with the
`monitor_idx` and `workspace_idx` given (if the workspace in question
doesn't have a wallpaper defined this won't do anything).

All these changes make it so the wallpaper theme generation colors are
properly applied in all situations.
2025-04-04 11:08:45 -07:00
alex-ds13
0e8ed8aa40 feat(wm): apply wallpapers per monitor 2025-04-04 11:08:45 -07:00
alex-ds13
fa2ccad5bf fix(wm): skip serde on WorkspaceGlobals 2025-04-04 11:08:45 -07:00
Csaba
3c4ccd2504 feat(bar): changing battery icons
This commit is changing the icon on the battery widget based on the
current level.

level | icon
------------
100 - 75: discharging
75 - 50: high
50 - 25: medium
25 - 10: low
10 - 0: warning

PR: #1398
2025-04-03 15:04:23 -07:00
LGUG2Z
7d821cd3db refactor(clippy): apply new rust lint fixes 2025-04-03 08:41:55 -07:00
Csaba
f4bbee0a2e feat(bar): auto select/hide widget based on value
This commit adds new settings to some widgets that allows to auto
select/hide them based on their current values.

The cpu/memory/network/storage widgets get a setting that auto selects
the widget if the current value/percentage is over a value.

The battery widget gets a setting that auto selects the widget if the
current percentage is under a value.

The storage widget gets a setting that auto hides the disk widget if the
percentage is under a value.

Also added 2 new settings (auto_select_fill and auto_select_text) to the
theme, in order to select the fill and text colors of an auto selected
widget.

(Easter egg: the network icons change if the value is over the limit)

PR: #1353
2025-04-03 07:42:27 -07:00
Lenus Walker
2934d011fd docs(mkdocs): add nvidia bar transparency workaround
Updated troubleshooting.md with the workaround for an issue where the
Komorebi Bar does not render transparency properly with Nvidia GPUs.
2025-04-02 17:22:06 -07:00
LGUG2Z
71762a5961 feat(cli): add datadir cmd
Because I'm tired of trying to find this stupid directory in
explorer.exe all the time.
2025-04-02 17:20:35 -07:00
zepocas
76aeefa9f7 feat(cli): add focused-workspace-layout query
This commit adds StateQuery::FocusedWorkspaceLayout variant to allow
queries for focused workspace layout via komorebic query.

This handles floating workspaces returning "None".
2025-04-02 15:33:46 -07:00
LGUG2Z
4968b0fe37 feat(themes): generate base16 palette from wallpaper
This commit adds a new Wallpaper configuration option to the
WorkspaceConfig, allowing the user to specify a path to a wallpaper
image file, and to specify whether to generate a base16 palette from the
colours of that image file.

Theme generation is enabled by default when a wallpaper is selected.

A set of theme options can be given to customize the colours of various
borders and accents.

The themes generated are also plumbed through to the komorebi-bar.

The palette generation algorithm from "flavours" (which has been forked
and updated) is quite slow, so the outputs are cached to file in
DATA_DIR, and keyed by ThemeVariant (Light or Dark).

The Win32 COM API to set the desktop wallpaper is also quite slow,
however this calls is async so it doesn't block komorebi's main thread.
2025-04-02 13:24:24 -07:00
LGUG2Z
b4b400b236 feat(themes): add custom base16 theme variant
This commit adds a custom Base16 theme variant and plumbs it throughout
the komorebi and komorebi-bar packages.
2025-04-02 13:23:31 -07:00
LGUG2Z
2ee0bbc0c7 refactor(themes): move colour.rs to komorebi-themes 2025-04-02 13:23:31 -07:00
Csaba
d38d3c956d feat(bar): add locked container widget, use accent colour for icons
This commit adds a new komorebi widget to indicate whether or not the
focused container is locked.

This commit also includes an icon colour change on the layer and layout
widgets to the accent colour.

The commit also renames the locked_window widget to locked_container as
it is more suitable.

PR: #1394
2025-04-02 13:23:02 -07:00
Csaba
052eb1c763 fix(bar): re-introduce retain exact workspace indices
This commit re-introduces a commit that was lost

original: 36e3eaad36

blame: bb31e7155d

fixes: https://github.com/LGUG2Z/komorebi/issues/1388
2025-04-01 10:08:22 -07:00
alex-ds13
58730b81b4 feat(gui): add floating and locked border colours 2025-04-01 05:53:33 -07:00
alex-ds13
274ae43e8f feat(wm): add unfocused_locked to border_colours
This commit adds the `unfocused_locked` color to the border_colours so
that users that don't use themes can still customize this color like
they do the others.
2025-04-01 05:53:26 -07:00
LGUG2Z
2a30f09bbd feat(cli): add version as state query variant
This commit adds a new StateQuery::Version, which allows integrators to
make decisions about how to handle different versions of komorebi's
state schema based on the version of komorebi that is running on a
user's machine.
2025-03-31 17:41:36 -07:00
alex-ds13
8fd18048a4 remove(wm): remove workspace_reconciliator 2025-03-31 15:23:33 -07:00
alex-ds13
5809735024 refactor(wm): refactor alt-tab reconciliation
This commit changes the way the alt-tab reconciliation is done.

It no longer uses the `workspace_reconciliator` with the sending of
notifications.

Instead there is a simple function that checks if the shown/uncloaked
window is already handled by komorebi and if it is it will check if it
is on an unfocused workspace and/or if it is an unfocused window of a
stacked container, returning the monitor/workspace index pair of said
window.

If we get this pair then we perform the reconciliation immediately by
focusing the monitor, workspace, container and window indices
corresponding to that window.
2025-03-31 15:23:14 -07:00
alex-ds13
96fdbbd1fb fix(client): re-export FloatingLayerBehaviour 2025-03-31 15:23:10 -07:00
Jerry Kingsbury
de131e9ca5 test(workspace): add more workspace tests
Added tests for the focus_container_by_window and
contains_managed_window functions.

test_focus_container_by_window crates two containers with different
distinct windows. The test checks to see if we are focused on the
expected container and window.

test_contains_managed_window creates two containers and checks to ensure
that when calling the function returns the expected result when checking
if a window is available in one of the containers.

Added a test for adding a new floating window. The test creates a
container with three windows and attempts to add one of the windows to
the list of floating windows, then checks to see if the first window in
the container was added to the floating window list and also checks the
list of windows and floating windows to ensure we get the expected
result.
2025-03-30 14:35:59 -07:00
LGUG2Z
07dba03255 docs(license): bump to 2.0.0 2025-03-30 12:59:41 -07:00
LGUG2Z
acd53dec1b feat(wm): add flag to set logging verbosity
This commit adds a --log-level flag to komorebi.exe which allows the
user to set the logging verbosity without setting the RUST_LOG
environment variable directly. If the RUST_LOG environment variable is
set, it will take precedence over the --log-level flag.
2025-03-28 19:03:07 -07:00
LGUG2Z
a98968d179 feat(config): add floating ws layer behaviour opt
This commit adds a new option to the WorkspaceConfig object,
floating_layer_behaviour, which allows the user to either set
FloatingLayerBehaviour::Tile or FloatingLayerBehaviour::Float. Although

I prefer Float as a default, there was a good enough argument to make
Tile the default based on the fact that the Floating layer is
automatically engaged based on the focused window, and previously when
the focused window was a floating window, new windows would be tiled
unless they matched floating rules.
2025-03-28 16:38:00 -07:00
LGUG2Z
8a32219867 docs(coc): move document from discord to github 2025-03-27 09:07:57 -07:00
Jerry Kingsbury
ce4b75cc3c test(monitor_reconciliator): add initial tests
Added a test for sending a MonitorNotification using the
send_notification function. The test sends a MonitorNotification and
tests that the notification was recieved using event_rx.

Added a test for inserting a monitor into the cache. The test creates
a monitor and uses the insert_in_monitor_cache function to insert the
monitor into cache, and will attempt to retrieve the monitor from the
cache and check to see if it matches the expected value.

Addaded a test for channel capacity. The test will send 20 notifications
and then another notification, then check to ensure that all 20
messages match the notification we sent. The test will also check to
ensure the last notification sent wasn't received.

Added a test for the notification listener. The test will create a
window manager and setup the notification listener, then check to see if
we can send a notification and receive the notification.

Created a Mock Monitor that can be used with testing functions that call
the Win32 API.

Added a test for the attach_display_devices function. The test will
create a Mock Monitor and add the monitor to a closure that simulates a
display_provider. The test passes the display_provider into the
attach_display_devices function and checks to ensure we recieve the
monitor we passed in.
2025-03-26 19:38:24 -07:00
alex-ds13
e4226ce623 feat(wm): show float windows in order on layer toggle
This commit raises and lowers all the floating windows so that they show
up with the smaller windows on top and the bigger windows on the bottom
when we toggle between layers.
2025-03-26 15:48:35 -07:00
alex-ds13
4bfd7febb4 feat(wm): focus floats depending on direction
This commit tries to focus the floating windows depending on the
direction used. It takes into account the top left corner of each window
and uses that to look for the closest top left corner in the direction
provided.

If there are no windows found in that direction, it then checks if it
can focus across monitor.
2025-03-26 15:48:15 -07:00
alex-ds13
5cc688dc6b feat(wm): track focused floating window
This commit changes the `floating_windows` from a `Vec<Window>` to a
`Ring<Window>` which allows us to keep track of the focused floating
window.

This combined with the existing layer switch allows us to know when we
should focus the focused container or the focused floating window.
2025-03-26 15:48:07 -07:00
LGUG2Z
d897890032 fix(bar): fall back to older pid icon lookups
This commit imports an older revision of my fork of windows-icons to
call when attempting to look up the icon of an application by it's
process id. This needs to be cleaned up before the next release.
2025-03-26 15:39:17 -07:00
LGUG2Z
e702d93a8a feat(cli): add move/send-to-last-workspace cmds
This commit adds two new komorebic commands, move-to-last-workspace and
send-to-last-workspace, which move or send the focused window to the
last focused workspace on the focused monitor.
2025-03-23 16:07:43 -07:00
LGUG2Z
a8c687d3d5 chore(deps): cargo update 2025-03-22 20:22:44 -07:00
alex-ds13
30fbc1ae73 feat(client): reexport win32_display_data
This commit reexports the `win32_display_data` crate so that any 3rd
party app that needs it can get it through the `komorebi-client` without
having to keep manually synchronizing it with the version used by komorebi.
2025-03-22 19:35:23 -07:00
LGUG2Z
cb60e91842 feat(bar): show icons for uwp apps
This commit integrates the excellent investigation and work done by
@davor-skontra on the windows-icons repo to enable the retrieval of UWP
applications, including all those annoying Microsoft applications which
all share the ApplicationFrameHost.exe executable and the
ApplicationFrameWindow class.

Since these applications share the same executable, the icon cache in
komorei-bar has been updated to use the window hwnd as a key intead of
the window executable.

resolve #1226
2025-03-22 19:31:30 -07:00
LGUG2Z
64d29d606a refactor(wm): add dep injection to monitor reconiliator
This commit adds some dependency injection to the monitor reconciliator
module to make it easier to test the behaviour when different kinds of
data are returned from win32_display_data.
2025-03-22 13:16:15 -07:00
LGUG2Z
072a62c314 perf(wm): clone-free locked deque insert/remove
This commit replaces my initial clumsy LockedDeque insert/remove
implementations with Clone-free implementations by @alex-ds13.
2025-03-22 12:33:40 -07:00
LGUG2Z
a95e6e9644 refactor(wm): consolidate handling of ws container insertion/removal 2025-03-22 12:33:39 -07:00
LGUG2Z
6ba19d3ea2 feat(wm): add locked containers per workspace
This commit adds the concept of locked container indexes to komorebi
workspaces.

When a container index is locked, it can only be displaced by manual
user actual - usually when another container is moved there, and when
this happens, that container becomes the locked container.

In the locked state, the container at the locked index should never be
displaced by new windows opening or existing windows around it being
closed.

When the total number of containers on a workspace falls below the
number of the locked index, the locked index will be removed.

A locked index can be identified by a special border color linked to the
new WindowKind::UnfocusedLocked variant.

The implementation of locked container indexes is backed by a new data
structure called a LockedDeque, which is a VecDeque with an auxiliary
HashSet which keeps track of locked indices.

A new komorebic command "toggle-lock" has been added to support
programmatic use of this feature, as well as the
LockMonitorWorkspaceContainer and UnlockMonitorWorkspaceContainer
SocketMessage variants which can be used by status bars.
2025-03-22 12:28:15 -07:00
LGUG2Z
edf1943157 chore(dev): begin v0.1.36-dev 2025-03-22 12:14:43 -07:00
LGUG2Z
d0c847e5bc docs(mkdocs): fix some broken relative links 2025-03-22 11:55:36 -07:00
LGUG2Z
992bc2abfe chore(release): v0.1.35 2025-03-20 21:17:55 -07:00
Jerry Kingsbury
fa07f2d2f8 test(monitor): add tests for ws and containers fns
Added tests around adding and removing workspaces and containers.
2025-03-20 18:36:40 -07:00
Jerry Kingsbury
cc4e204191 test(container): add tests for addition and removal
Added tests for the various add, remove, and contains functions.
2025-03-20 18:35:55 -07:00
alex-ds13
24791f0ce5 fix(wm): change when floating windows are centered
This commit changes the behaviour of when floating windows are centered
by making them only center when we are on a `WorkspaceLayer::Floating`
and the window doesn't match a `floating_windows` rule and the workspace
is not a floating workspace.
2025-03-20 18:34:35 -07:00
LGUG2Z
6b95bf95f9 fix(wm): unset all duplicate monitor serial ids
This commit fixes a rare issue, seen exclusively with Acer monitors so
far, where two monitors of the same model can have an identical serial
number id.

If we encounter a system which has two connected monitors with the same
serial id number, the serial id number will be forcefully unset and
blacklisted for the rest of the session.

In this case, users must fall back to using device_id for options like
display_index_preferences.

Possibly a little overkill, but since this has been such a headache I'm
going to opt for this approach over #1368 for now.
2025-03-20 09:08:47 -07:00
LGUG2Z
c0e1e9366d chore(deps): bump windows-rs from 0.60 to 0.61 2025-03-18 08:54:55 -07:00
Jerry Kingsbury
532436fe1a test(workspace): add container and window tests
Added tests for functions related to removing, adding, and focusing a
container and windows.
2025-03-18 08:48:11 -07:00
LGUG2Z
532949409c fix(borders): show regular cursor on hover
This commit ensures that the WM_SETCURSOR message is handled by border
windows by setting the cursor to the default IDC_ARROW, which prevents
the spinning "Loading" icon from showing on hover.

Some credit is due to the user steiner5 on Discord for whining about me
not being interested in working on this while more generally complaining
about komorebi, which consequently made me want to work on this out of
spite since he's probably going to bounce off the software anyway.
2025-03-17 19:18:40 -07:00
dependabot[bot]
ec4a5e6491 chore(deps): bump reqwest from 0.12.12 to 0.12.14
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.12.12 to 0.12.14.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/v0.12.14/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.12.12...v0.12.14)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-17 19:00:16 -07:00
dependabot[bot]
f6e99eaac1 chore(deps): bump uuid from 1.15.1 to 1.16.0
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.15.1 to 1.16.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.15.1...v1.16.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-17 19:00:09 -07:00
dependabot[bot]
a6cf801a6b chore(deps): bump starship-battery from 0.10.0 to 0.10.1
Bumps [starship-battery](https://github.com/starship/rust-battery) from 0.10.0 to 0.10.1.
- [Release notes](https://github.com/starship/rust-battery/releases)
- [Changelog](https://github.com/starship/rust-battery/blob/main/CHANGELOG.md)
- [Commits](https://github.com/starship/rust-battery/compare/v0.10.0...v0.10.1)

---
updated-dependencies:
- dependency-name: starship-battery
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-17 19:00:02 -07:00
Jerry Kingsbury
83d11c6f0f test(workspace): add container manipulation tests
Added tests for the different container adding and removing functions.

Also added a test for the contains_window function.
2025-03-17 18:32:47 -07:00
Jerry Kingsbury
1ba1c57ba0 test(wm): add cycle window tests
Added a test for the cycle_window_in_direction function. The function is
tested by creating 3 windows and cycling in both the next a previous
direction. The test will ensure that the windows were cycled by checking
the window index to ensure it is the expected window.

Created a test for cycling the window by index. This test is similar to
the other cycle window test and performs the same steps as that one,
except it uses the test_cycle_container_index_in_direction function
instead.
2025-03-17 18:20:47 -07:00
alex-ds13
9f16894a09 fix(wm): correct workspace restore + remove workarounds
This commit removes some workarounds on the `update_focused_workspace`
function that were there to fix issues related to some bugs on the
`workspace.restore()` function.

This commit fixes the bugs on the `restore` function instead. The
`update_focused_workspace` function should be used only to update a
workspace layout.
2025-03-17 18:17:30 -07:00
alex-ds13
df9ae931cc feat(wm): toggle float override with floating layer
This commit ensures that when the user switches to the flooating layer
any new spawned window will be spawned floating as if the float override
was set.

This results in a workflow which makes it easier to spawn floating
windows on the fly.

This effectively reverts commit ff2aa5e51a.
2025-03-17 18:16:16 -07:00
alex-ds13
be2af9fdcb fix(borders): update monitor_idx on all borders
This fixes the issue where borders moved to other monitors would
sometimes be removed by their initial monitor because this data wasn't
being updated.

This effectively reverts commit 5919f88b38.
2025-03-17 18:15:27 -07:00
alex-ds13
c083484ef0 fix(wm): correctly focus workspace on eager-focus 2025-03-17 18:12:51 -07:00
Jerry Kingsbury
1804b21c4a test(wm): add tests for ws movement + tiling state
Added a test for moving a workspace from one monitor to another. The
test will ensure that after calling the function, the current monitor
index is the one where the workspace was transfered to. Test also will
check the count to ensure the new workspace was added to the new monitor
and that the old monitor has one less.

Added a test for swapping monitor workspaces. The test will create two
monitors one with a workspace that has a container containing multiple
windows and another with one. The test calls the swap_monitor_workspaces
function and then checks to see if the two workspaces were successfully
swapped.

Added a test to test toggling the tiling state. After each switch the
test will test the current state to ensure that it is the expected
state.
2025-03-16 15:55:00 -07:00
LGUG2Z
b6bd191cf5 feat(wm): center windows spawned during float override
It's very annoying and unclear when enabling the float override to open
the next window as floating to have that window appear immediately over
an existing tile.

This commit sets an explicit behaviour to center windows spawned while
the float override is active.
2025-03-16 15:48:28 -07:00
LGUG2Z
5919f88b38 fix(borders): do multiple render passes when required
This commit addresses a border rendering issue when moving a window from
a higher-indexed monitor to a lower-indexed monitor.

Previously, we would do a single render pass across all monitors in
order of their indexes, destroying borders no longer needed, and
creating new borders for new windows.

This resulted in the window being moved to the lower-indexed monitor
still existing in the global border cache when that monitor's borders
were updated, but then being removed when the borders of the origin,
higher-indexed monitor were updated.

With the changes in this commit, if we encounter a situation like this,
an additional render pass will be executed to ensure that the window
will have a corresponding border created on the destination
lower-indexed monitor.
2025-03-16 14:33:22 -07:00
LGUG2Z
ff2aa5e51a feat(wm): toggle float override with floating layer
This commit ensures that a workspace float override will also be applied
when the user switches to the floating layer, and removed when the user
switches to the tiling layer. This results in a workflow which makes it
easier to spawn floating windows on the fly.
2025-03-16 13:32:18 -07:00
LGUG2Z
42c12d5ec3 docs(mkdocs): add initial pages of usage section 2025-03-15 20:28:14 -07:00
LGUG2Z
f0ce8e8572 fix(wm): focus when switching to ws with only floating windows
This commit ensures that if a user switches from a workspace with
managed windows to a workspace without any managed windows but only
floating windows, the focused window from the previous workspace will
lose focus as it should, and the first floating window on the new
workspace will gain focus as it should.
2025-03-15 13:34:31 -07:00
Csaba
bdea4821c3 refactor(bar): move widgets to new folder
This commit moves all the widgets to a new folder in order to organize
the code while hoping to make it easier to find and add a widget.
2025-03-15 09:47:45 -07:00
Jerry Kingsbury
229aeb7ddc test(wm): add tests for additions, removals and swaps
Added a test for removing a window from a container. The test checks to
ensure a new container is created and that the removed window from the
original container is added to the new container. Also ensures that the
old container has one less container after the function is called.

Added a test for removing a workspace. The test keeps track of the
number of workspaces and the current workspace index.

Added test for swapping containers from different monitors. The test
will create two monitors and containers with different amount of
windows. Test will verify that the swap is successful by checking that
the number of windows in the container matches the number of windows it
was created with in the previous monitor. The same will be done with the
other container.
2025-03-15 09:45:57 -07:00
alex-ds13
17cbdc8663 docs(mkdocs): add multi monitor common-workflows section 2025-03-14 16:51:02 -07:00
Csaba
9f3d806f79 fix(bar): limit new state freq on time and date
This commit limits the number of times the time and date widgets get a
new state.

Even though having a limit of 1 second on the ui repaint, when the bar
is focused or hovered, this is ignored and state of these widgets were
updated many more times a second.

The time widget looked less accurate and lagging (especially with
multiple showing seconds) so the refresh interval is set to be 500 ms
instead of 1 second.
2025-03-14 15:59:51 -07:00
LGUG2Z
fe9a1416e7 feat(config): allow multiple asc files
This commit allows either the single canonical applications.json file,
or multiple files which adhere to the asc scheme to be given to the
app_specific_configuration_path config option.

I thought I had already implemented this earlier, but evidently I
hadn't.

This will be useful for people who want to maintain their own
independent set of asc rules, as they can be kept in a dedicated file
which won't be overwritten by the fetch-asc command.

resolve #736
2025-03-13 19:35:14 -07:00
Csaba
3618beb366 fix(bar): add focus-monitor-at-cursor msg to buttons
This commit adds the FocusMonitorAtCursor SocketMessage on all the
toggle buttons as the monitor idx is not sent as a parameter and the
monitor needs to be focused when these buttons are clicked on the bar
(especially for multiple bars).
2025-03-13 16:57:32 -07:00
alex-ds13
a4de2ee841 fix(wm): use preferred id when caching monitors
Use the preferred id set by the user as the key when caching monitors.
This way if a user is having issues with the device_id on their system
they can use the serial_number_id on display_index_preferences.

If instead a user is having issues with the serial_number_id they can
make sure to use the device_id on display_index_preferences. Komorebi
will use the preferred one as a key when caching the monitors.

This PR also includes a change of the DISPLAY_INDEX_PREFERENCES from a
Mutex to RwLock.

I believe the RwLock is better since it allows multiple readers at the
same time, while the mutex blocks on all calls even if you just want to
read or compare some value.

For display index preferences (same thing applies to other existing
mutexes) most times we access it is a read-only access. We only ever
change it when applying a config or when a command is used to change it.

Every other use is a read-only use so it doesn't make sense to block on
those situations
2025-03-13 09:12:15 -07:00
alex-ds13
60e1834b43 fix(wm): correct float window move/resize
This commit fixes an issue where the move/resize functions for floating
windows weren't properly taking into account the coordinates of
secondary monitors and were only working correctly on the main monitor
where top/left was 0/0.
2025-03-11 17:58:05 -07:00
alex-ds13
54323c4c6a fix(wm): update layer on cross monitor moves 2025-03-11 15:19:27 -07:00
LGUG2Z
6516c808ee chore(deps): cargo update 2025-03-10 20:52:42 -07:00
Jerry Kingsbury
894b6f3d96 test(wm): add container transfer and cycle tests
Implemented drop to ensure that the socket is deleted whenever the
function goes out of scope. This will ensure that if the test fails, the
test socket file will still be removed.

Added a test for cycling to the next and previous container in a
workspace.

Added a test for transfering a window from Monitor 1 to Monitor 2

Added a test for transfering a container to another montior. Also
updated the transfer window test to transfer the window back to the
original container. Both tests will check both monitors to ensure the
expected number of containers are in both.
2025-03-10 20:43:55 -07:00
dependabot[bot]
7ccdff4986 chore(deps): bump netdev from 0.32.0 to 0.33.0
Bumps [netdev](https://github.com/shellrow/netdev) from 0.32.0 to 0.33.0.
- [Release notes](https://github.com/shellrow/netdev/releases)
- [Commits](https://github.com/shellrow/netdev/compare/v0.32.0...v0.33.0)

---
updated-dependencies:
- dependency-name: netdev
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 08:33:09 -07:00
dependabot[bot]
c48e1db0ff chore(deps): bump random_word from 0.4.3 to 0.5.0
Bumps [random_word](https://github.com/MitchellRhysHall/random_word) from 0.4.3 to 0.5.0.
- [Commits](https://github.com/MitchellRhysHall/random_word/commits)

---
updated-dependencies:
- dependency-name: random_word
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 08:32:59 -07:00
alex-ds13
ea9752d5e1 fix(wm): focus correct window if monocled stack
Previously if we had a stack on a monocle container and tried to cycle
stack or move the window within the stack or even using the focus stack
window from a bar it would focus the wrong window and temporarely show
that wrong window. This commit fixes this.
2025-03-10 08:23:49 -07:00
Csaba
acf780767c feat(bar): add changing icons based on the time
This commit adds a little Easter egg on the time widget.

Use the `changing_icon` setting to enable this feature.

Based on the current time, the widget will use different icons to
indicate certain activities of the day.

00:00 MOON
06:00 ALARM
06:01 BREAD
06:30 BARBELL
08:00 COFFEE
08:30 CLOCK
12:00 HAMBURGER
12:30 CLOCK_AFTERNOON
18:00 FORK_KNIFE
18:30 MOON_STARS
2025-03-09 11:48:46 -07:00
Jerry Kingsbury
8e588d0284 test(wm): add workspace and monitor tests
Added tests to test switching monitor focus, setting workspace name, and
monitor size.
2025-03-08 13:49:51 -08:00
dependabot[bot]
91ff9b8852 chore(deps): bump ring from 0.17.11 to 0.17.13
Bumps [ring](https://github.com/briansmith/ring) from 0.17.11 to 0.17.13.
- [Changelog](https://github.com/briansmith/ring/blob/main/RELEASES.md)
- [Commits](https://github.com/briansmith/ring/commits)

---
updated-dependencies:
- dependency-name: ring
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-07 16:47:46 -08:00
Csaba
81a7951312 feat(bar): added timezone to time and date widgets
This commit adds the timezone on the time and date widgets as a new
setting.

In case the timezone is invalid, the output is replaced with an error
message.

Use a custom format to display additional information.

resolve #1312
2025-03-07 16:28:45 -08:00
LGUG2Z
555308db5f test(wm): don't rely on datadir for socket location
This commit avoids relying on DATA_DIR for the test socket location as
this path is not available to us in GitHub Actions runners.
2025-03-07 16:10:16 -08:00
LGUG2Z
c90769f5fa chore(deny): allow unmaintained crate paste 2025-03-07 16:01:41 -08:00
LGUG2Z
76002385ab test(wm): colocate tests with src files, add socket msg test 2025-03-06 19:39:59 -08:00
Jerry Kingsbury
f40e80cd61 test(wm): add window manager unit tests
Created a test that creates the WM instance and ensures the instance is
running. The test creates a custom socket and then cleans up the socket
file after completion.

Created a test that creates a WM instance, monitor instance, and
workpace. The tests checks to ensure that the expected workspace is
focused properly.

Included recommended fixes to ensure that the focus_workspace function
is used correctly and that the test accurately checks the workspaces
length, current workspace index, and switching to an existing workspace.
2025-03-06 15:34:30 -08:00
LGUG2Z
e4f9d8af86 chore(deps): add deny.toml and dependencies.json 2025-03-06 15:34:27 -08:00
LGUG2Z
0c64432c25 feat(cli): add cycle-empty-workspace cmd
This commit adds a variation of the cycle-workspace command which will
attempt to focus the next empty workspace in the given direction (with
the usual wraparound). If there are no empty workspaces available, this
command will do nothing.
2025-03-05 15:52:21 -08:00
LGUG2Z
fe20caa56a fix(client): expose schemars feature and allow opt-out 2025-03-05 15:49:46 -08:00
Csaba
02a2796e7d fix(bar): correct time widget binary format alignment
This commit adds a bit of offset to the time widget's binary clocks so
they are more in the middle.

It also fixes some visual changes that were caused by upgrading to
eframe 0.31
2025-03-04 14:57:21 -08:00
LGUG2Z
a0eb025cec fix(bar): add opt schemars derive on test case 2025-03-04 08:39:47 -08:00
alex-ds13
70a61376a8 fix(borders): ignore borders on all stack window updates 2025-03-04 08:20:35 -08:00
LGUG2Z
1761919707 perf(client): disable schemars feature on komorebi lib dep 2025-03-03 21:49:43 -08:00
LGUG2Z
1325da4e81 chore(deps): cargo update 2025-03-03 21:46:03 -08:00
alex-ds13
0776ca1565 feat(border): cache borders on all workspaces
This commit refactors the `border_manager` with the following changes:

- Rework the way the borders are created and their pointer is sent to
  the window message handler. Now we also store the same pointer as a
  `Box<Border>` on the `border_manager`'s `BORDER_STATE`. This means
  that the borders we have are exactly the same ones that the border
  window itself is accessing so we can now store the border's info
  inside the `Border` struct and all of it will be accessible by the
  border window as well. This makes it so the "ACTUAL" border struct is
  the one created on the thread of the `Border::create()` function and
  when that thread finishes (after the border window closes) it will
  handle the drop of the border itself. However this means we need to be
  careful with our own stored `Box<Border>` since it will point to the
  same memory we can't let the compiler dropped them as usual or
  otherwise it would create heap corruption errors. So this commit
  creates a special function called `destroy_border()` to actually close
  the window border without dropping the border, since it will be later
  dropped by the thread that created it.

- Remove `BORDERS_MONITORS`, `FOCUS_STATE` and `RENDER_TARGETS` arc
  mutexes, since now this info is stored on the border itself.

- Change the `BORDER_STATE` to now map an id (container or window) to a
  `Box<Border>`.

- Change the `WINDOWS_BORDERS` to now map a window hwnd to a border id.

- Create new struct `BorderInfo` which as the border window hwnd and the
  border kind. This struct is what is now returned by the function
  `window_border()` which checks if some window as a border attached to
  it and if it does it returns this info. There is no need to clone the
  entire border. If in the future we need more info we can just add it
  to this struct.

- Change the way we clear the `BORDER_STATE`. Like mentioned before we
  need to be sure we don't drop the `Box<Border>` when removing it, so
  now we use the `.drain` function to remove all the borders as an
  iterator and we call the `destroy_border()` on each border we are
  removing.

- We now check if a border's `tracking_hwnd` has changed and if it does
  we simply update it instead of destroying the border and create a new
  one.

- Create function `delete_border` so that we can remove a border
  properly from outside the `border_manager`.

- Create function `hide_border` which spawns a new thread that searches
  if a window hwnd has a border attached to it and if it does it hides
  said border window. This function is called on every `window.hide()`.

- Create function `show_border` which spawns a new thread that searches
  if a window hwnd has a border attached to it and if it does it restores
  said border window. This function is called on every `window.restore()`.

- This commit also changes the previous `window.hide()` and
  `window.restore()` functions to be named:
  - `window.hide_with_border(hide_border: bool)`: this is the same
    function as before but adds a check at the end in case `hide_border`
    is true it calls `border_manager::hide_border()`. A new function was
    created with the same name as before `window.hide()` which by
    default calls this new function with `hide_border = true`.
  - `window.restore_with_border(restore_border: bool)`: this is the same
    function as before but adds a check at the end in case `hide_border`
    is true it calls `border_manager::hide_border()`. A new function was
    created with the same name as before `window.hide()` which by
    default calls this new function with `hide_border = true`.

- This commit creates a new function on `Container` called
  `load_focused_window_ignore_borders()` which performs the same as
  `load_focused_window()` but it ignores the borders when hiding and
  restoring the windows. This function, along with the
  `hide_with_border(false)` and `restore_with_border(false)` are used on
  all functions related to changing focus on a stack since if we let the
  borders be hidden and restored when cycling or changing focus on a
  stack the border would flicker slightly, this prevents that. Ignore
  borders when clicking on the stackbar as well.
  (P.S. there might still be other places that I forgot to use these new
  functions, but if that is the case then what will happen is a simple
  flicker of the stack border...)

- The `remove_window` from `Workspace` needs to call the
  `border_manager::delete_border()` so that wew make sure we remove that
  windows's border window as well if it exists. This is essential when
  enforcing workspace rules, otherwise the border would be left behind.

- Lastly, but not least, now that we hide the borders windows along with
  their tracking window, we no longer remove the borders when swapping
  workspaces or when toggling monocle, etc. Instead we keep all borders
  of all workspaces cached and simply hide them. They are only removed
  when their tracking window is closed or cloaked on a stack (since on a
  stack we only keep one border for all the entire stack container).
  This means that when changing between workspaces we no longer see the
  borders showing up delayed after the windows show up. Now both the
  window and it's border show up as if they are one and the same.
2025-03-03 21:15:45 -08:00
alex-ds13
724b0b7692 fix(border): update border when moving from admin windows 2025-03-03 21:13:37 -08:00
LGUG2Z
b53de81754 perf(cargo): make schemars derives optional
This commit makes all schemars::JsonSchema derives optional. After
analyzing the output of cargo build timings and llvm-lines, it was clear
that the majority of the 2m+ incremental dev build times was taken up by
codegen, and the majority of it by schemars.

Developers can now run cargo commands with --no-default-features to
disable schemars::JsonSchema codegen, and all justfile commands have
been updated to take this flag by default, with the exception of the
jsonschema target, which will compile with all derives required to
export the various jsonschema files.

Incremental dev build times for komorebi.exe on my machine are now at
around ~18s, while clean dev build times for the entire workspace are at
around ~1m.
2025-03-03 21:12:46 -08:00
108 changed files with 14007 additions and 2956 deletions

View File

@@ -18,6 +18,14 @@ on:
workflow_dispatch:
jobs:
cargo-deny:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: EmbarkStudios/cargo-deny-action@v2
build:
strategy:
fail-fast: true
@@ -47,7 +55,7 @@ jobs:
key: ${{ matrix.platform.target }}
- run: cargo +nightly fmt --check
- run: cargo clippy
- run: cargo test --package komorebi --test compat
- run: cargo test
- uses: houseabsolute/actions-rust-cross@v1
with:
command: "build"

40
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,40 @@
# The Komorebi Code of Conduct
This document is based on the [Rust Code of
Conduct](https://www.rust-lang.org/policies/code-of-conduct)
## Conduct
- We are committed to providing a friendly, safe and welcoming environment for
all, regardless of level of experience, gender identity and expression, sexual
orientation, disability, personal appearance, body size, race, ethnicity, age,
religion, nationality, or other similar characteristic.
- Please avoid using overtly sexual aliases or other nicknames that might
detract from a friendly, safe and welcoming environment for all.
- Please be kind and courteous. Theres no need to be mean or rude.
- Respect that people have differences of opinion and that every design or
implementation choice carries a trade-off and numerous costs. There is seldom a
right answer.
- Please keep unstructured critique to a minimum. If you have solid ideas you
want to experiment with, make a fork and see how it works.
- We will exclude you from interaction if you insult, demean or harass anyone.
That is not welcome behavior. We interpret the term “harassment” as including
the definition in the [Citizen Code of
Conduct](https://github.com/stumpsyn/policies/blob/master/citizen_code_of_conduct.md);
if you have any lack of clarity about what might be included in that concept,
please read their definition. In particular, we dont tolerate behavior that
excludes people in socially marginalized groups.
- Private harassment is also unacceptable. No matter who you are, if you feel
you have been or are being harassed or made uncomfortable by a community member,
please contact me immediately. Whether youre a regular contributor or a
newcomer, we care about making this community a safe place for you and weve got
your back.
- Likewise any spamming, trolling, flaming, baiting or other attention-stealing
behavior is not welcome.

1889
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,7 @@ members = [
[workspace.dependencies]
clap = { version = "4", features = ["derive", "wrap_help"] }
chrono-tz = "0.10"
chrono = "0.4"
crossbeam-channel = "0.5"
crossbeam-utils = "0.8"
@@ -34,16 +35,16 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
paste = "1"
sysinfo = "0.33"
uds_windows = "1"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "55cebdebfbd68dbd14945a1ba90f6b05b7be2893" }
windows-numerics = { version = "0.1" }
windows-implement = { version = "0.59" }
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "a28c6559a9de2f92c142a714947a9b081776caca" }
windows-numerics = { version = "0.2" }
windows-implement = { version = "0.60" }
windows-interface = { version = "0.59" }
windows-core = { version = "0.60" }
windows-core = { version = "0.61" }
shadow-rs = "1"
which = "7"
[workspace.dependencies.windows]
version = "0.60"
version = "0.61"
features = [
"Foundation_Numerics",
"Win32_Devices",
@@ -71,17 +72,4 @@ features = [
"Win32_System_WindowsProgramming",
"Media",
"Media_Control"
]
[profile.dev-jeezy]
inherits = "dev"
debug = false
opt-level = 1
[profile.dev-jeezy.package."*"]
opt-level = 3
[profile.release-jeezy]
inherits = "release"
incremental = true
codegen-units = 256
]

View File

@@ -1,6 +1,6 @@
# Komorebi License
Version 1.0.0
Version 2.0.0
## Acceptance
@@ -13,9 +13,20 @@ your licenses.
The licensor grants you a copyright license for the software
to do everything you might do with the software that would
otherwise infringe the licensor's copyright in it for any
permitted purpose. However, you may only make changes according
permitted purpose. However, you may only distribute the source
code of the software according to the [Distribution License](
#distribution-license), you may only make changes according
to the [Changes License](#changes-license), and you may not
distribute the software or new works based on the software.
otherwise distribute the software or new works based on the
software.
## Distribution License
The licensor grants you an additional copyright license to
distribute copies of the source code of the software. Your
license to distribute covers distributing the source code of
the software with changes permitted by the [Changes License](
#changes-license).
## Changes License
@@ -45,7 +56,7 @@ law. These terms do not limit them.
These terms do not allow you to sublicense or transfer any of
your licenses to anyone else, or prevent the licensor from
granting licenses to anyone else. These terms do not imply
granting licenses to anyone else. These terms do not imply
any other licenses.
## Patent Defense
@@ -63,7 +74,7 @@ violated any of these terms, or done anything with the software
not covered by your licenses, your licenses can nonetheless
continue if you come into full compliance with these terms,
and take practical steps to correct past violations, within
32 days of receiving notice. Otherwise, all your licenses
32 days of receiving notice. Otherwise, all your licenses
end immediately.
## No Liability
@@ -88,11 +99,10 @@ organizations that have control over, are under the control of,
or are under common control with that organization. **Control**
means ownership of substantially all the assets of an entity,
or the power to direct its management and policies by vote,
contract, or otherwise. Control can be direct or indirect.
contract, or otherwise. Control can be direct or indirect.
**Your licenses** are all the licenses granted to you for the
software under these terms.
**Use** means anything you do with the software requiring one
of your licenses.

View File

@@ -71,7 +71,10 @@ showcases the many awesome projects that exist in the _komorebi_ ecosystem.
## Licensing for Personal Use
`komorebi` is licensed under the [Komorebi 1.0.0
`komorebi` is [educational source
software](https://lgug2z.com/articles/educational-source-software/).
`komorebi` is licensed under the [Komorebi 2.0.0
license](https://github.com/LGUG2Z/komorebi-license), which is a fork of the
[PolyForm Strict 1.0.0
license](https://polyformproject.org/licenses/strict/1.0.0). On a high level
@@ -82,7 +85,7 @@ hard-forks) based on the software.
Anyone is free to make their own fork of `komorebi` with changes intended either
for personal use or for integration back upstream via pull requests.
The [Komorebi 1.0.0 License](https://github.com/LGUG2Z/komorebi-license) does
The [Komorebi 2.0.0 License](https://github.com/LGUG2Z/komorebi-license) does
not permit any kind of commercial use (i.e. using `komorebi` at work).
## Sponsorship for Personal Use
@@ -99,7 +102,8 @@ me on GitHub.
[GitHub Sponsors is enabled for this
project](https://github.com/sponsors/LGUG2Z). Sponsors can claim custom roles on
the Discord server, get shout outs at the end of _komorebi_-related videos on
YouTube, and gain the ability to submit feature requests on the issue tracker.
YouTube, gain the ability to submit feature requests on the issue tracker, and
receive releases of komorebi with "easter eggs" on physical media.
If you would like to tip or sponsor the project but are unable to use GitHub
Sponsors, you may also sponsor through [Ko-fi](https://ko-fi.com/lgug2z), or
@@ -142,7 +146,8 @@ video will answer the majority of your questions.
[@amnweb](https://github.com/amnweb) showing _komorebi_ `v0.1.28` running on Windows 11 with window borders,
unfocused window transparency and animations enabled, using a custom status bar integrated using
_komorebi_'s [Window Manager Event Subscriptions](https://github.com/LGUG2Z/komorebi?tab=readme-ov-file#window-manager-event-subscriptions).
_komorebi_'
s [Window Manager Event Subscriptions](https://github.com/LGUG2Z/komorebi?tab=readme-ov-file#window-manager-event-subscriptions).
https://github.com/LGUG2Z/komorebi/assets/13164844/21be8dc4-fa76-4f70-9b37-1d316f4b40c2
@@ -389,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.34"}
// komorebi-client = { git = "https://github.com/LGUG2Z/komorebi", tag = "v0.1.36"}
use anyhow::Result;
use komorebi_client::Notification;

99
deny.toml Normal file
View File

@@ -0,0 +1,99 @@
[graph]
targets = [
"x86_64-pc-windows-msvc",
"i686-pc-windows-msvc",
"aarch64-pc-windows-msvc",
]
all-features = false
no-default-features = false
[output]
feature-depth = 1
[advisories]
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" }
]
[licenses]
allow = [
"0BSD",
"Apache-2.0",
"Artistic-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"BSL-1.0",
"CC0-1.0",
"ISC",
"MIT",
"MIT-0",
"MPL-2.0",
"OFL-1.1",
"Ubuntu-font-1.0",
"Unicode-3.0",
"Zlib",
"LicenseRef-Komorebi-1.0"
]
confidence-threshold = 0.8
[[licenses.clarify]]
crate = "komorebi"
expression = "LicenseRef-Komorebi-1.0"
license-files = []
[[licenses.clarify]]
crate = "komorebi-client"
expression = "LicenseRef-Komorebi-1.0"
license-files = []
[[licenses.clarify]]
crate = "komorebic"
expression = "LicenseRef-Komorebi-1.0"
license-files = []
[[licenses.clarify]]
crate = "komorebic-no-console"
expression = "LicenseRef-Komorebi-1.0"
license-files = []
[[licenses.clarify]]
crate = "komorebi-themes"
expression = "LicenseRef-Komorebi-1.0"
license-files = []
[[licenses.clarify]]
crate = "komorebi-gui"
expression = "LicenseRef-Komorebi-1.0"
license-files = []
[[licenses.clarify]]
crate = "komorebi-bar"
expression = "LicenseRef-Komorebi-1.0"
license-files = []
[[licenses.clarify]]
crate = "base16-egui-themes"
expression = "MIT"
license-files = []
[bans]
multiple-versions = "allow"
wildcards = "allow"
highlight = "all"
workspace-default-features = "allow"
external-default-features = "allow"
[sources]
unknown-registry = "deny"
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = [
"https://github.com/LGUG2Z/base16-egui-themes",
"https://github.com/LGUG2Z/catppuccin-egui",
"https://github.com/LGUG2Z/windows-icons",
"https://github.com/LGUG2Z/win32-display-data",
"https://github.com/LGUG2Z/flavours",
"https://github.com/LGUG2Z/base16_color_scheme",
]

899
dependencies.json Normal file
View File

@@ -0,0 +1,899 @@
{
"licenses": [
[
"0BSD",
[
"adler 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
"adler2 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
"win32-display-data 0.1.0 git+https://github.com/LGUG2Z/win32-display-data?rev=a28c6559a9de2f92c142a714947a9b081776caca"
]
],
[
"Apache-2.0",
[
"ab_glyph 0.2.29 registry+https://github.com/rust-lang/crates.io-index",
"ab_glyph_rasterizer 0.1.8 registry+https://github.com/rust-lang/crates.io-index",
"accesskit 0.17.1 registry+https://github.com/rust-lang/crates.io-index",
"accesskit_consumer 0.26.0 registry+https://github.com/rust-lang/crates.io-index",
"accesskit_windows 0.24.1 registry+https://github.com/rust-lang/crates.io-index",
"accesskit_winit 0.23.1 registry+https://github.com/rust-lang/crates.io-index",
"adler 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
"adler2 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
"ahash 0.8.11 registry+https://github.com/rust-lang/crates.io-index",
"anstream 0.6.18 registry+https://github.com/rust-lang/crates.io-index",
"anstyle 1.0.10 registry+https://github.com/rust-lang/crates.io-index",
"anstyle-parse 0.2.6 registry+https://github.com/rust-lang/crates.io-index",
"anstyle-query 1.1.2 registry+https://github.com/rust-lang/crates.io-index",
"anstyle-wincon 3.0.7 registry+https://github.com/rust-lang/crates.io-index",
"anyhow 1.0.97 registry+https://github.com/rust-lang/crates.io-index",
"approx 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
"arboard 3.5.0 registry+https://github.com/rust-lang/crates.io-index",
"arrayvec 0.7.6 registry+https://github.com/rust-lang/crates.io-index",
"atomic-waker 1.1.2 registry+https://github.com/rust-lang/crates.io-index",
"autocfg 1.4.0 registry+https://github.com/rust-lang/crates.io-index",
"backtrace 0.3.71 registry+https://github.com/rust-lang/crates.io-index",
"backtrace-ext 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"base16_color_scheme 0.3.2 git+https://github.com/LGUG2Z/base16_color_scheme",
"base64 0.22.1 registry+https://github.com/rust-lang/crates.io-index",
"beef 0.5.2 registry+https://github.com/rust-lang/crates.io-index",
"bit_field 0.10.2 registry+https://github.com/rust-lang/crates.io-index",
"bitflags 1.3.2 registry+https://github.com/rust-lang/crates.io-index",
"bitflags 2.9.0 registry+https://github.com/rust-lang/crates.io-index",
"bitstream-io 2.6.0 registry+https://github.com/rust-lang/crates.io-index",
"bytemuck 1.22.0 registry+https://github.com/rust-lang/crates.io-index",
"bytemuck_derive 1.9.3 registry+https://github.com/rust-lang/crates.io-index",
"cc 1.2.19 registry+https://github.com/rust-lang/crates.io-index",
"cfg-if 0.1.10 registry+https://github.com/rust-lang/crates.io-index",
"cfg-if 1.0.0 registry+https://github.com/rust-lang/crates.io-index",
"chrono 0.4.40 registry+https://github.com/rust-lang/crates.io-index",
"chrono-tz 0.10.3 registry+https://github.com/rust-lang/crates.io-index",
"chrono-tz-build 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
"clap 4.5.36 registry+https://github.com/rust-lang/crates.io-index",
"clap_builder 4.5.36 registry+https://github.com/rust-lang/crates.io-index",
"clap_derive 4.5.32 registry+https://github.com/rust-lang/crates.io-index",
"clap_lex 0.7.4 registry+https://github.com/rust-lang/crates.io-index",
"color-eyre 0.6.3 registry+https://github.com/rust-lang/crates.io-index",
"color-spantrace 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"colorchoice 1.0.3 registry+https://github.com/rust-lang/crates.io-index",
"crc32fast 1.4.2 registry+https://github.com/rust-lang/crates.io-index",
"crossbeam-channel 0.5.15 registry+https://github.com/rust-lang/crates.io-index",
"crossbeam-deque 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
"crossbeam-epoch 0.9.18 registry+https://github.com/rust-lang/crates.io-index",
"crossbeam-utils 0.8.21 registry+https://github.com/rust-lang/crates.io-index",
"ctrlc 3.4.6 registry+https://github.com/rust-lang/crates.io-index",
"cursor-icon 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"deflate 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
"deranged 0.4.0 registry+https://github.com/rust-lang/crates.io-index",
"dirs 3.0.2 registry+https://github.com/rust-lang/crates.io-index",
"dirs 4.0.0 registry+https://github.com/rust-lang/crates.io-index",
"dirs 6.0.0 registry+https://github.com/rust-lang/crates.io-index",
"dirs-sys 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
"dirs-sys 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"displaydoc 0.2.5 registry+https://github.com/rust-lang/crates.io-index",
"document-features 0.2.11 registry+https://github.com/rust-lang/crates.io-index",
"dpi 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"dunce 1.0.5 registry+https://github.com/rust-lang/crates.io-index",
"dyn-clone 1.0.19 registry+https://github.com/rust-lang/crates.io-index",
"ecolor 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
"eframe 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
"egui 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
"egui-phosphor 0.9.0 registry+https://github.com/rust-lang/crates.io-index",
"egui-winit 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
"egui_extras 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
"egui_glow 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
"either 1.15.0 registry+https://github.com/rust-lang/crates.io-index",
"emath 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
"encoding_rs 0.8.35 registry+https://github.com/rust-lang/crates.io-index",
"enum-map 2.7.3 registry+https://github.com/rust-lang/crates.io-index",
"enum-map-derive 0.17.0 registry+https://github.com/rust-lang/crates.io-index",
"env_home 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"epaint 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
"epaint_default_fonts 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
"equivalent 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
"eyre 0.6.12 registry+https://github.com/rust-lang/crates.io-index",
"fastrand 2.3.0 registry+https://github.com/rust-lang/crates.io-index",
"fdeflate 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
"filetime 0.2.25 registry+https://github.com/rust-lang/crates.io-index",
"flate2 1.1.1 registry+https://github.com/rust-lang/crates.io-index",
"fnv 1.0.7 registry+https://github.com/rust-lang/crates.io-index",
"form_urlencoded 1.2.1 registry+https://github.com/rust-lang/crates.io-index",
"futures 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-channel 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-core 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-executor 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-io 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-macro 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-sink 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-task 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-util 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"getrandom 0.1.16 registry+https://github.com/rust-lang/crates.io-index",
"getrandom 0.2.15 registry+https://github.com/rust-lang/crates.io-index",
"getrandom 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
"gif 0.11.4 registry+https://github.com/rust-lang/crates.io-index",
"gif 0.13.1 registry+https://github.com/rust-lang/crates.io-index",
"git2 0.20.1 registry+https://github.com/rust-lang/crates.io-index",
"gl_generator 0.14.0 registry+https://github.com/rust-lang/crates.io-index",
"glob 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
"glow 0.16.0 registry+https://github.com/rust-lang/crates.io-index",
"glutin 0.32.2 registry+https://github.com/rust-lang/crates.io-index",
"glutin_egl_sys 0.7.1 registry+https://github.com/rust-lang/crates.io-index",
"glutin_wgl_sys 0.6.1 registry+https://github.com/rust-lang/crates.io-index",
"half 2.6.0 registry+https://github.com/rust-lang/crates.io-index",
"hashbrown 0.12.3 registry+https://github.com/rust-lang/crates.io-index",
"hashbrown 0.15.2 registry+https://github.com/rust-lang/crates.io-index",
"heck 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"hex 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
"hex_color 3.0.0 registry+https://github.com/rust-lang/crates.io-index",
"hotwatch 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"http 1.3.1 registry+https://github.com/rust-lang/crates.io-index",
"httparse 1.10.1 registry+https://github.com/rust-lang/crates.io-index",
"hyper-tls 0.6.0 registry+https://github.com/rust-lang/crates.io-index",
"iana-time-zone 0.1.63 registry+https://github.com/rust-lang/crates.io-index",
"ident_case 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
"idna 1.0.3 registry+https://github.com/rust-lang/crates.io-index",
"idna_adapter 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
"image 0.25.6 registry+https://github.com/rust-lang/crates.io-index",
"image-webp 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"imgref 1.11.0 registry+https://github.com/rust-lang/crates.io-index",
"immutable-chunkmap 2.0.6 registry+https://github.com/rust-lang/crates.io-index",
"indenter 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
"indexmap 1.9.3 registry+https://github.com/rust-lang/crates.io-index",
"indexmap 2.9.0 registry+https://github.com/rust-lang/crates.io-index",
"ipnet 2.11.0 registry+https://github.com/rust-lang/crates.io-index",
"is_debug 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"is_terminal_polyfill 1.70.1 registry+https://github.com/rust-lang/crates.io-index",
"itertools 0.12.1 registry+https://github.com/rust-lang/crates.io-index",
"itertools 0.14.0 registry+https://github.com/rust-lang/crates.io-index",
"itoa 1.0.15 registry+https://github.com/rust-lang/crates.io-index",
"jobserver 0.1.33 registry+https://github.com/rust-lang/crates.io-index",
"jpeg-decoder 0.1.22 registry+https://github.com/rust-lang/crates.io-index",
"jpeg-decoder 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
"khronos_api 3.1.0 registry+https://github.com/rust-lang/crates.io-index",
"lazy_static 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
"libc 0.2.171 registry+https://github.com/rust-lang/crates.io-index",
"libgit2-sys 0.18.1+1.9.0 registry+https://github.com/rust-lang/crates.io-index",
"libz-sys 1.1.22 registry+https://github.com/rust-lang/crates.io-index",
"linked-hash-map 0.5.6 registry+https://github.com/rust-lang/crates.io-index",
"litrs 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
"lock_api 0.4.12 registry+https://github.com/rust-lang/crates.io-index",
"log 0.4.27 registry+https://github.com/rust-lang/crates.io-index",
"logos 0.14.4 registry+https://github.com/rust-lang/crates.io-index",
"logos-codegen 0.14.4 registry+https://github.com/rust-lang/crates.io-index",
"logos-derive 0.14.4 registry+https://github.com/rust-lang/crates.io-index",
"miette 7.5.0 registry+https://github.com/rust-lang/crates.io-index",
"miette-derive 7.5.0 registry+https://github.com/rust-lang/crates.io-index",
"mime 0.3.17 registry+https://github.com/rust-lang/crates.io-index",
"minimal-lexical 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"miniz_oxide 0.4.4 registry+https://github.com/rust-lang/crates.io-index",
"miniz_oxide 0.8.8 registry+https://github.com/rust-lang/crates.io-index",
"miow 0.6.0 registry+https://github.com/rust-lang/crates.io-index",
"native-tls 0.2.14 registry+https://github.com/rust-lang/crates.io-index",
"net2 0.2.39 registry+https://github.com/rust-lang/crates.io-index",
"nohash-hasher 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"ntapi 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
"num 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
"num-bigint 0.4.6 registry+https://github.com/rust-lang/crates.io-index",
"num-complex 0.4.6 registry+https://github.com/rust-lang/crates.io-index",
"num-conv 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"num-derive 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
"num-integer 0.1.46 registry+https://github.com/rust-lang/crates.io-index",
"num-iter 0.1.45 registry+https://github.com/rust-lang/crates.io-index",
"num-rational 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
"num-rational 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
"num-traits 0.2.19 registry+https://github.com/rust-lang/crates.io-index",
"once_cell 1.21.3 registry+https://github.com/rust-lang/crates.io-index",
"owned_ttf_parser 0.25.0 registry+https://github.com/rust-lang/crates.io-index",
"palette 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"palette_derive 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"parking_lot 0.12.3 registry+https://github.com/rust-lang/crates.io-index",
"parking_lot_core 0.9.10 registry+https://github.com/rust-lang/crates.io-index",
"paste 1.0.15 registry+https://github.com/rust-lang/crates.io-index",
"percent-encoding 2.3.1 registry+https://github.com/rust-lang/crates.io-index",
"pin-project-lite 0.2.16 registry+https://github.com/rust-lang/crates.io-index",
"pin-utils 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"pkg-config 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
"png 0.16.8 registry+https://github.com/rust-lang/crates.io-index",
"png 0.17.16 registry+https://github.com/rust-lang/crates.io-index",
"powerfmt 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"ppv-lite86 0.2.21 registry+https://github.com/rust-lang/crates.io-index",
"proc-macro-error-attr2 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
"proc-macro-error2 2.0.1 registry+https://github.com/rust-lang/crates.io-index",
"proc-macro2 1.0.94 registry+https://github.com/rust-lang/crates.io-index",
"profiling 1.0.16 registry+https://github.com/rust-lang/crates.io-index",
"profiling-procmacros 1.0.16 registry+https://github.com/rust-lang/crates.io-index",
"qoi 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
"quick-error 2.0.1 registry+https://github.com/rust-lang/crates.io-index",
"quote 1.0.40 registry+https://github.com/rust-lang/crates.io-index",
"rand 0.7.3 registry+https://github.com/rust-lang/crates.io-index",
"rand 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
"rand_chacha 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
"rand_chacha 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
"rand_core 0.5.1 registry+https://github.com/rust-lang/crates.io-index",
"rand_core 0.6.4 registry+https://github.com/rust-lang/crates.io-index",
"rand_pcg 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"raw-window-handle 0.6.2 registry+https://github.com/rust-lang/crates.io-index",
"rayon 1.10.0 registry+https://github.com/rust-lang/crates.io-index",
"rayon-core 1.12.1 registry+https://github.com/rust-lang/crates.io-index",
"regex 1.11.1 registry+https://github.com/rust-lang/crates.io-index",
"regex-automata 0.4.9 registry+https://github.com/rust-lang/crates.io-index",
"regex-syntax 0.6.29 registry+https://github.com/rust-lang/crates.io-index",
"regex-syntax 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
"reqwest 0.12.15 registry+https://github.com/rust-lang/crates.io-index",
"roxmltree 0.20.0 registry+https://github.com/rust-lang/crates.io-index",
"rustc-demangle 0.1.24 registry+https://github.com/rust-lang/crates.io-index",
"rustls-pemfile 2.2.0 registry+https://github.com/rust-lang/crates.io-index",
"rustls-pki-types 1.11.0 registry+https://github.com/rust-lang/crates.io-index",
"rustversion 1.0.20 registry+https://github.com/rust-lang/crates.io-index",
"ryu 1.0.20 registry+https://github.com/rust-lang/crates.io-index",
"scopeguard 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
"serde 1.0.219 registry+https://github.com/rust-lang/crates.io-index",
"serde_derive 1.0.219 registry+https://github.com/rust-lang/crates.io-index",
"serde_derive_internals 0.29.1 registry+https://github.com/rust-lang/crates.io-index",
"serde_json 1.0.140 registry+https://github.com/rust-lang/crates.io-index",
"serde_json_lenient 0.2.4 registry+https://github.com/rust-lang/crates.io-index",
"serde_urlencoded 0.7.1 registry+https://github.com/rust-lang/crates.io-index",
"serde_variant 0.1.3 registry+https://github.com/rust-lang/crates.io-index",
"serde_with 3.12.0 registry+https://github.com/rust-lang/crates.io-index",
"serde_with_macros 3.12.0 registry+https://github.com/rust-lang/crates.io-index",
"serde_yaml 0.8.26 registry+https://github.com/rust-lang/crates.io-index",
"serde_yaml 0.9.34+deprecated registry+https://github.com/rust-lang/crates.io-index",
"shadow-rs 1.1.1 registry+https://github.com/rust-lang/crates.io-index",
"shell-words 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"shellexpand 2.1.2 registry+https://github.com/rust-lang/crates.io-index",
"shlex 1.3.0 registry+https://github.com/rust-lang/crates.io-index",
"siphasher 0.3.11 registry+https://github.com/rust-lang/crates.io-index",
"siphasher 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
"smallvec 1.15.0 registry+https://github.com/rust-lang/crates.io-index",
"smol_str 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
"socket2 0.5.9 registry+https://github.com/rust-lang/crates.io-index",
"stable_deref_trait 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
"static_assertions 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"supports-color 3.0.2 registry+https://github.com/rust-lang/crates.io-index",
"supports-hyperlinks 3.1.0 registry+https://github.com/rust-lang/crates.io-index",
"supports-unicode 3.0.0 registry+https://github.com/rust-lang/crates.io-index",
"syn 1.0.109 registry+https://github.com/rust-lang/crates.io-index",
"syn 2.0.100 registry+https://github.com/rust-lang/crates.io-index",
"sync_wrapper 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
"tempfile 3.19.1 registry+https://github.com/rust-lang/crates.io-index",
"terminal_size 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
"thiserror 1.0.69 registry+https://github.com/rust-lang/crates.io-index",
"thiserror 2.0.12 registry+https://github.com/rust-lang/crates.io-index",
"thiserror-impl 1.0.69 registry+https://github.com/rust-lang/crates.io-index",
"thiserror-impl 2.0.12 registry+https://github.com/rust-lang/crates.io-index",
"thread_local 1.1.8 registry+https://github.com/rust-lang/crates.io-index",
"time 0.3.41 registry+https://github.com/rust-lang/crates.io-index",
"time-core 0.1.4 registry+https://github.com/rust-lang/crates.io-index",
"toml 0.5.11 registry+https://github.com/rust-lang/crates.io-index",
"ttf-parser 0.25.1 registry+https://github.com/rust-lang/crates.io-index",
"typenum 1.18.0 registry+https://github.com/rust-lang/crates.io-index",
"tz-rs 0.7.0 registry+https://github.com/rust-lang/crates.io-index",
"tzdb 0.7.2 registry+https://github.com/rust-lang/crates.io-index",
"unicase 2.8.1 registry+https://github.com/rust-lang/crates.io-index",
"unicode-ident 1.0.18 registry+https://github.com/rust-lang/crates.io-index",
"unicode-linebreak 0.1.5 registry+https://github.com/rust-lang/crates.io-index",
"unicode-segmentation 1.12.0 registry+https://github.com/rust-lang/crates.io-index",
"unicode-width 0.1.14 registry+https://github.com/rust-lang/crates.io-index",
"unicode-width 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"unicode-xid 0.2.6 registry+https://github.com/rust-lang/crates.io-index",
"uom 0.36.0 registry+https://github.com/rust-lang/crates.io-index",
"url 2.5.4 registry+https://github.com/rust-lang/crates.io-index",
"utf16_iter 1.0.5 registry+https://github.com/rust-lang/crates.io-index",
"utf8_iter 1.0.4 registry+https://github.com/rust-lang/crates.io-index",
"utf8parse 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
"vcpkg 0.2.15 registry+https://github.com/rust-lang/crates.io-index",
"version_check 0.9.5 registry+https://github.com/rust-lang/crates.io-index",
"web-time 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"webbrowser 1.0.4 registry+https://github.com/rust-lang/crates.io-index",
"weezl 0.1.8 registry+https://github.com/rust-lang/crates.io-index",
"winapi 0.3.9 registry+https://github.com/rust-lang/crates.io-index",
"windows 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
"windows 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
"windows 0.60.0 registry+https://github.com/rust-lang/crates.io-index",
"windows 0.61.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-collections 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-collections 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-core 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-core 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-core 0.60.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-core 0.61.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-future 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-future 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-implement 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-implement 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-implement 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-implement 0.60.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-interface 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-interface 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-interface 0.59.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-link 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-numerics 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-numerics 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-registry 0.4.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-result 0.1.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-result 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-result 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-strings 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-strings 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-strings 0.4.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-sys 0.48.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-sys 0.52.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-sys 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-targets 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
"windows-targets 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
"windows-targets 0.53.0 registry+https://github.com/rust-lang/crates.io-index",
"windows_aarch64_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
"windows_aarch64_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
"windows_aarch64_msvc 0.53.0 registry+https://github.com/rust-lang/crates.io-index",
"windows_i686_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
"windows_i686_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
"windows_i686_msvc 0.53.0 registry+https://github.com/rust-lang/crates.io-index",
"windows_x86_64_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
"windows_x86_64_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
"windows_x86_64_msvc 0.53.0 registry+https://github.com/rust-lang/crates.io-index",
"winit 0.30.9 registry+https://github.com/rust-lang/crates.io-index",
"wmi 0.15.2 registry+https://github.com/rust-lang/crates.io-index",
"write16 1.0.0 registry+https://github.com/rust-lang/crates.io-index",
"yaml-rust 0.4.5 registry+https://github.com/rust-lang/crates.io-index",
"zerocopy 0.7.35 registry+https://github.com/rust-lang/crates.io-index",
"zerocopy 0.8.24 registry+https://github.com/rust-lang/crates.io-index",
"zune-core 0.4.12 registry+https://github.com/rust-lang/crates.io-index",
"zune-inflate 0.2.54 registry+https://github.com/rust-lang/crates.io-index",
"zune-jpeg 0.4.14 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"Artistic-2.0",
[
"file-id 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"notify-debouncer-full 0.1.0 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"BSD-2-Clause",
[
"av1-grain 0.2.3 registry+https://github.com/rust-lang/crates.io-index",
"rav1e 0.7.1 registry+https://github.com/rust-lang/crates.io-index",
"v_frame 0.3.8 registry+https://github.com/rust-lang/crates.io-index",
"zerocopy 0.7.35 registry+https://github.com/rust-lang/crates.io-index",
"zerocopy 0.8.24 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"BSD-3-Clause",
[
"alloc-no-stdlib 2.0.4 registry+https://github.com/rust-lang/crates.io-index",
"alloc-stdlib 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
"avif-serialize 0.8.3 registry+https://github.com/rust-lang/crates.io-index",
"brotli 3.5.0 registry+https://github.com/rust-lang/crates.io-index",
"brotli-decompressor 2.5.1 registry+https://github.com/rust-lang/crates.io-index",
"encoding_rs 0.8.35 registry+https://github.com/rust-lang/crates.io-index",
"exr 1.73.0 registry+https://github.com/rust-lang/crates.io-index",
"lebe 0.5.2 registry+https://github.com/rust-lang/crates.io-index",
"ravif 0.11.11 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"BSL-1.0",
[
"clipboard-win 5.4.0 registry+https://github.com/rust-lang/crates.io-index",
"error-code 3.3.1 registry+https://github.com/rust-lang/crates.io-index",
"ryu 1.0.20 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"CC0-1.0",
[
"dunce 1.0.5 registry+https://github.com/rust-lang/crates.io-index",
"file-id 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"imgref 1.11.0 registry+https://github.com/rust-lang/crates.io-index",
"notify 6.1.1 registry+https://github.com/rust-lang/crates.io-index",
"notify-debouncer-full 0.1.0 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"ISC",
[
"is_ci 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
"libloading 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
"rustls-pemfile 2.2.0 registry+https://github.com/rust-lang/crates.io-index",
"starship-battery 0.10.1 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"MIT",
[
"accesskit 0.17.1 registry+https://github.com/rust-lang/crates.io-index",
"accesskit_consumer 0.26.0 registry+https://github.com/rust-lang/crates.io-index",
"accesskit_windows 0.24.1 registry+https://github.com/rust-lang/crates.io-index",
"adler 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
"adler2 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
"ahash 0.8.11 registry+https://github.com/rust-lang/crates.io-index",
"aho-corasick 1.1.3 registry+https://github.com/rust-lang/crates.io-index",
"aligned-vec 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"anstream 0.6.18 registry+https://github.com/rust-lang/crates.io-index",
"anstyle 1.0.10 registry+https://github.com/rust-lang/crates.io-index",
"anstyle-parse 0.2.6 registry+https://github.com/rust-lang/crates.io-index",
"anstyle-query 1.1.2 registry+https://github.com/rust-lang/crates.io-index",
"anstyle-wincon 3.0.7 registry+https://github.com/rust-lang/crates.io-index",
"anyhow 1.0.97 registry+https://github.com/rust-lang/crates.io-index",
"arboard 3.5.0 registry+https://github.com/rust-lang/crates.io-index",
"arg_enum_proc_macro 0.3.4 registry+https://github.com/rust-lang/crates.io-index",
"arrayvec 0.7.6 registry+https://github.com/rust-lang/crates.io-index",
"atomic-waker 1.1.2 registry+https://github.com/rust-lang/crates.io-index",
"autocfg 1.4.0 registry+https://github.com/rust-lang/crates.io-index",
"backtrace 0.3.71 registry+https://github.com/rust-lang/crates.io-index",
"backtrace-ext 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"base16_color_scheme 0.3.2 git+https://github.com/LGUG2Z/base16_color_scheme",
"base64 0.22.1 registry+https://github.com/rust-lang/crates.io-index",
"beef 0.5.2 registry+https://github.com/rust-lang/crates.io-index",
"bit_field 0.10.2 registry+https://github.com/rust-lang/crates.io-index",
"bitflags 1.3.2 registry+https://github.com/rust-lang/crates.io-index",
"bitflags 2.9.0 registry+https://github.com/rust-lang/crates.io-index",
"bitstream-io 2.6.0 registry+https://github.com/rust-lang/crates.io-index",
"brotli 3.5.0 registry+https://github.com/rust-lang/crates.io-index",
"brotli-decompressor 2.5.1 registry+https://github.com/rust-lang/crates.io-index",
"built 0.7.7 registry+https://github.com/rust-lang/crates.io-index",
"bytemuck 1.22.0 registry+https://github.com/rust-lang/crates.io-index",
"bytemuck_derive 1.9.3 registry+https://github.com/rust-lang/crates.io-index",
"byteorder 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
"byteorder-lite 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"bytes 1.10.1 registry+https://github.com/rust-lang/crates.io-index",
"calm_io 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"calmio_filters 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"catppuccin-egui 5.3.1 git+https://github.com/LGUG2Z/catppuccin-egui?rev=bdaff30959512c4f7ee7304117076a48633d777f",
"cc 1.2.19 registry+https://github.com/rust-lang/crates.io-index",
"cfg-if 0.1.10 registry+https://github.com/rust-lang/crates.io-index",
"cfg-if 1.0.0 registry+https://github.com/rust-lang/crates.io-index",
"cfg_aliases 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"chrono 0.4.40 registry+https://github.com/rust-lang/crates.io-index",
"chrono-tz 0.10.3 registry+https://github.com/rust-lang/crates.io-index",
"chrono-tz-build 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
"clap 4.5.36 registry+https://github.com/rust-lang/crates.io-index",
"clap_builder 4.5.36 registry+https://github.com/rust-lang/crates.io-index",
"clap_derive 4.5.32 registry+https://github.com/rust-lang/crates.io-index",
"clap_lex 0.7.4 registry+https://github.com/rust-lang/crates.io-index",
"color-eyre 0.6.3 registry+https://github.com/rust-lang/crates.io-index",
"color-spantrace 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"color-thief 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
"color_quant 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"colorchoice 1.0.3 registry+https://github.com/rust-lang/crates.io-index",
"crc32fast 1.4.2 registry+https://github.com/rust-lang/crates.io-index",
"crossbeam-channel 0.5.15 registry+https://github.com/rust-lang/crates.io-index",
"crossbeam-deque 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
"crossbeam-epoch 0.9.18 registry+https://github.com/rust-lang/crates.io-index",
"crossbeam-utils 0.8.21 registry+https://github.com/rust-lang/crates.io-index",
"ctrlc 3.4.6 registry+https://github.com/rust-lang/crates.io-index",
"cursor-icon 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"darling 0.20.11 registry+https://github.com/rust-lang/crates.io-index",
"darling_core 0.20.11 registry+https://github.com/rust-lang/crates.io-index",
"darling_macro 0.20.11 registry+https://github.com/rust-lang/crates.io-index",
"deflate 0.8.6 registry+https://github.com/rust-lang/crates.io-index",
"deranged 0.4.0 registry+https://github.com/rust-lang/crates.io-index",
"dirs 3.0.2 registry+https://github.com/rust-lang/crates.io-index",
"dirs 4.0.0 registry+https://github.com/rust-lang/crates.io-index",
"dirs 6.0.0 registry+https://github.com/rust-lang/crates.io-index",
"dirs-sys 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
"dirs-sys 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"displaydoc 0.2.5 registry+https://github.com/rust-lang/crates.io-index",
"document-features 0.2.11 registry+https://github.com/rust-lang/crates.io-index",
"dyn-clone 1.0.19 registry+https://github.com/rust-lang/crates.io-index",
"ecolor 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
"eframe 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
"egui 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
"egui-phosphor 0.9.0 registry+https://github.com/rust-lang/crates.io-index",
"egui-winit 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
"egui_extras 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
"egui_glow 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
"either 1.15.0 registry+https://github.com/rust-lang/crates.io-index",
"emath 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
"encoding_rs 0.8.35 registry+https://github.com/rust-lang/crates.io-index",
"enum-map 2.7.3 registry+https://github.com/rust-lang/crates.io-index",
"enum-map-derive 0.17.0 registry+https://github.com/rust-lang/crates.io-index",
"env_home 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"epaint 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
"epaint_default_fonts 0.31.1 registry+https://github.com/rust-lang/crates.io-index",
"equivalent 1.0.2 registry+https://github.com/rust-lang/crates.io-index",
"eyre 0.6.12 registry+https://github.com/rust-lang/crates.io-index",
"fastrand 2.3.0 registry+https://github.com/rust-lang/crates.io-index",
"fdeflate 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
"filetime 0.2.25 registry+https://github.com/rust-lang/crates.io-index",
"flate2 1.1.1 registry+https://github.com/rust-lang/crates.io-index",
"flavours 0.7.2 git+https://github.com/LGUG2Z/flavours",
"fnv 1.0.7 registry+https://github.com/rust-lang/crates.io-index",
"font-loader 0.11.0 registry+https://github.com/rust-lang/crates.io-index",
"form_urlencoded 1.2.1 registry+https://github.com/rust-lang/crates.io-index",
"fs-tail 0.1.4 registry+https://github.com/rust-lang/crates.io-index",
"futures 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-channel 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-core 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-executor 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-io 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-macro 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-sink 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-task 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"futures-util 0.3.31 registry+https://github.com/rust-lang/crates.io-index",
"getrandom 0.1.16 registry+https://github.com/rust-lang/crates.io-index",
"getrandom 0.2.15 registry+https://github.com/rust-lang/crates.io-index",
"getrandom 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
"getset 0.1.5 registry+https://github.com/rust-lang/crates.io-index",
"gif 0.11.4 registry+https://github.com/rust-lang/crates.io-index",
"gif 0.13.1 registry+https://github.com/rust-lang/crates.io-index",
"git2 0.20.1 registry+https://github.com/rust-lang/crates.io-index",
"glob 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
"glow 0.16.0 registry+https://github.com/rust-lang/crates.io-index",
"glutin-winit 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"h2 0.4.8 registry+https://github.com/rust-lang/crates.io-index",
"half 2.6.0 registry+https://github.com/rust-lang/crates.io-index",
"hashbrown 0.12.3 registry+https://github.com/rust-lang/crates.io-index",
"hashbrown 0.15.2 registry+https://github.com/rust-lang/crates.io-index",
"heck 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"hex 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
"hex_color 3.0.0 registry+https://github.com/rust-lang/crates.io-index",
"hotwatch 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"http 1.3.1 registry+https://github.com/rust-lang/crates.io-index",
"http-body 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
"http-body-util 0.1.3 registry+https://github.com/rust-lang/crates.io-index",
"httparse 1.10.1 registry+https://github.com/rust-lang/crates.io-index",
"hyper 1.6.0 registry+https://github.com/rust-lang/crates.io-index",
"hyper-tls 0.6.0 registry+https://github.com/rust-lang/crates.io-index",
"hyper-util 0.1.11 registry+https://github.com/rust-lang/crates.io-index",
"iana-time-zone 0.1.63 registry+https://github.com/rust-lang/crates.io-index",
"ident_case 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
"idna 1.0.3 registry+https://github.com/rust-lang/crates.io-index",
"idna_adapter 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
"image 0.23.14 registry+https://github.com/rust-lang/crates.io-index",
"image 0.25.6 registry+https://github.com/rust-lang/crates.io-index",
"image-webp 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"immutable-chunkmap 2.0.6 registry+https://github.com/rust-lang/crates.io-index",
"indenter 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
"indexmap 1.9.3 registry+https://github.com/rust-lang/crates.io-index",
"indexmap 2.9.0 registry+https://github.com/rust-lang/crates.io-index",
"ipnet 2.11.0 registry+https://github.com/rust-lang/crates.io-index",
"is_debug 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"is_terminal_polyfill 1.70.1 registry+https://github.com/rust-lang/crates.io-index",
"itertools 0.12.1 registry+https://github.com/rust-lang/crates.io-index",
"itertools 0.14.0 registry+https://github.com/rust-lang/crates.io-index",
"itoa 1.0.15 registry+https://github.com/rust-lang/crates.io-index",
"jobserver 0.1.33 registry+https://github.com/rust-lang/crates.io-index",
"jpeg-decoder 0.1.22 registry+https://github.com/rust-lang/crates.io-index",
"jpeg-decoder 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
"lazy_static 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
"libc 0.2.171 registry+https://github.com/rust-lang/crates.io-index",
"libgit2-sys 0.18.1+1.9.0 registry+https://github.com/rust-lang/crates.io-index",
"libz-sys 1.1.22 registry+https://github.com/rust-lang/crates.io-index",
"linked-hash-map 0.5.6 registry+https://github.com/rust-lang/crates.io-index",
"litrs 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
"lock_api 0.4.12 registry+https://github.com/rust-lang/crates.io-index",
"log 0.4.27 registry+https://github.com/rust-lang/crates.io-index",
"logos 0.14.4 registry+https://github.com/rust-lang/crates.io-index",
"logos-codegen 0.14.4 registry+https://github.com/rust-lang/crates.io-index",
"logos-derive 0.14.4 registry+https://github.com/rust-lang/crates.io-index",
"loop9 0.1.5 registry+https://github.com/rust-lang/crates.io-index",
"matchers 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"maybe-rayon 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"memchr 2.7.4 registry+https://github.com/rust-lang/crates.io-index",
"memoffset 0.9.1 registry+https://github.com/rust-lang/crates.io-index",
"mime 0.3.17 registry+https://github.com/rust-lang/crates.io-index",
"mime_guess2 2.3.0 registry+https://github.com/rust-lang/crates.io-index",
"minimal-lexical 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"miniz_oxide 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
"miniz_oxide 0.4.4 registry+https://github.com/rust-lang/crates.io-index",
"miniz_oxide 0.8.8 registry+https://github.com/rust-lang/crates.io-index",
"mio 1.0.3 registry+https://github.com/rust-lang/crates.io-index",
"miow 0.6.0 registry+https://github.com/rust-lang/crates.io-index",
"nanoid 0.4.0 registry+https://github.com/rust-lang/crates.io-index",
"native-tls 0.2.14 registry+https://github.com/rust-lang/crates.io-index",
"net2 0.2.39 registry+https://github.com/rust-lang/crates.io-index",
"netdev 0.33.0 registry+https://github.com/rust-lang/crates.io-index",
"new_debug_unreachable 1.0.6 registry+https://github.com/rust-lang/crates.io-index",
"nohash-hasher 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"nom 7.1.3 registry+https://github.com/rust-lang/crates.io-index",
"noop_proc_macro 0.3.0 registry+https://github.com/rust-lang/crates.io-index",
"ntapi 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
"nu-ansi-term 0.46.0 registry+https://github.com/rust-lang/crates.io-index",
"num 0.4.3 registry+https://github.com/rust-lang/crates.io-index",
"num-bigint 0.4.6 registry+https://github.com/rust-lang/crates.io-index",
"num-complex 0.4.6 registry+https://github.com/rust-lang/crates.io-index",
"num-conv 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"num-derive 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
"num-integer 0.1.46 registry+https://github.com/rust-lang/crates.io-index",
"num-iter 0.1.45 registry+https://github.com/rust-lang/crates.io-index",
"num-rational 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
"num-rational 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
"num-traits 0.2.19 registry+https://github.com/rust-lang/crates.io-index",
"once_cell 1.21.3 registry+https://github.com/rust-lang/crates.io-index",
"os_info 3.10.0 registry+https://github.com/rust-lang/crates.io-index",
"overload 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"owo-colors 3.5.0 registry+https://github.com/rust-lang/crates.io-index",
"owo-colors 4.2.0 registry+https://github.com/rust-lang/crates.io-index",
"palette 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"palette_derive 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"parking_lot 0.12.3 registry+https://github.com/rust-lang/crates.io-index",
"parking_lot_core 0.9.10 registry+https://github.com/rust-lang/crates.io-index",
"parse-zoneinfo 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
"paste 1.0.15 registry+https://github.com/rust-lang/crates.io-index",
"percent-encoding 2.3.1 registry+https://github.com/rust-lang/crates.io-index",
"phf 0.11.3 registry+https://github.com/rust-lang/crates.io-index",
"phf 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
"phf_codegen 0.11.3 registry+https://github.com/rust-lang/crates.io-index",
"phf_codegen 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
"phf_generator 0.11.3 registry+https://github.com/rust-lang/crates.io-index",
"phf_generator 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
"phf_shared 0.11.3 registry+https://github.com/rust-lang/crates.io-index",
"phf_shared 0.8.0 registry+https://github.com/rust-lang/crates.io-index",
"pin-project-lite 0.2.16 registry+https://github.com/rust-lang/crates.io-index",
"pin-utils 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"pkg-config 0.3.32 registry+https://github.com/rust-lang/crates.io-index",
"png 0.16.8 registry+https://github.com/rust-lang/crates.io-index",
"png 0.17.16 registry+https://github.com/rust-lang/crates.io-index",
"powerfmt 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"powershell_script 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"ppv-lite86 0.2.21 registry+https://github.com/rust-lang/crates.io-index",
"proc-macro-error-attr2 2.0.0 registry+https://github.com/rust-lang/crates.io-index",
"proc-macro-error2 2.0.1 registry+https://github.com/rust-lang/crates.io-index",
"proc-macro2 1.0.94 registry+https://github.com/rust-lang/crates.io-index",
"profiling 1.0.16 registry+https://github.com/rust-lang/crates.io-index",
"profiling-procmacros 1.0.16 registry+https://github.com/rust-lang/crates.io-index",
"qoi 0.4.1 registry+https://github.com/rust-lang/crates.io-index",
"quick-error 2.0.1 registry+https://github.com/rust-lang/crates.io-index",
"quote 1.0.40 registry+https://github.com/rust-lang/crates.io-index",
"rand 0.7.3 registry+https://github.com/rust-lang/crates.io-index",
"rand 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
"rand_chacha 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
"rand_chacha 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
"rand_core 0.5.1 registry+https://github.com/rust-lang/crates.io-index",
"rand_core 0.6.4 registry+https://github.com/rust-lang/crates.io-index",
"rand_pcg 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"random_word 0.5.0 registry+https://github.com/rust-lang/crates.io-index",
"raw-window-handle 0.6.2 registry+https://github.com/rust-lang/crates.io-index",
"rayon 1.10.0 registry+https://github.com/rust-lang/crates.io-index",
"rayon-core 1.12.1 registry+https://github.com/rust-lang/crates.io-index",
"regex 1.11.1 registry+https://github.com/rust-lang/crates.io-index",
"regex-automata 0.1.10 registry+https://github.com/rust-lang/crates.io-index",
"regex-automata 0.4.9 registry+https://github.com/rust-lang/crates.io-index",
"regex-syntax 0.6.29 registry+https://github.com/rust-lang/crates.io-index",
"regex-syntax 0.8.5 registry+https://github.com/rust-lang/crates.io-index",
"reqwest 0.12.15 registry+https://github.com/rust-lang/crates.io-index",
"rgb 0.8.50 registry+https://github.com/rust-lang/crates.io-index",
"roxmltree 0.20.0 registry+https://github.com/rust-lang/crates.io-index",
"rustc-demangle 0.1.24 registry+https://github.com/rust-lang/crates.io-index",
"rustls-pemfile 2.2.0 registry+https://github.com/rust-lang/crates.io-index",
"rustls-pki-types 1.11.0 registry+https://github.com/rust-lang/crates.io-index",
"rustversion 1.0.20 registry+https://github.com/rust-lang/crates.io-index",
"same-file 1.0.6 registry+https://github.com/rust-lang/crates.io-index",
"schannel 0.1.27 registry+https://github.com/rust-lang/crates.io-index",
"schemars 0.8.22 registry+https://github.com/rust-lang/crates.io-index",
"schemars_derive 0.8.22 registry+https://github.com/rust-lang/crates.io-index",
"scoped_threadpool 0.1.9 registry+https://github.com/rust-lang/crates.io-index",
"scopeguard 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
"serde 1.0.219 registry+https://github.com/rust-lang/crates.io-index",
"serde_derive 1.0.219 registry+https://github.com/rust-lang/crates.io-index",
"serde_derive_internals 0.29.1 registry+https://github.com/rust-lang/crates.io-index",
"serde_json 1.0.140 registry+https://github.com/rust-lang/crates.io-index",
"serde_json_lenient 0.2.4 registry+https://github.com/rust-lang/crates.io-index",
"serde_urlencoded 0.7.1 registry+https://github.com/rust-lang/crates.io-index",
"serde_variant 0.1.3 registry+https://github.com/rust-lang/crates.io-index",
"serde_with 3.12.0 registry+https://github.com/rust-lang/crates.io-index",
"serde_with_macros 3.12.0 registry+https://github.com/rust-lang/crates.io-index",
"serde_yaml 0.8.26 registry+https://github.com/rust-lang/crates.io-index",
"serde_yaml 0.9.34+deprecated registry+https://github.com/rust-lang/crates.io-index",
"shadow-rs 1.1.1 registry+https://github.com/rust-lang/crates.io-index",
"sharded-slab 0.1.7 registry+https://github.com/rust-lang/crates.io-index",
"shell-words 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"shellexpand 2.1.2 registry+https://github.com/rust-lang/crates.io-index",
"shlex 1.3.0 registry+https://github.com/rust-lang/crates.io-index",
"simd-adler32 0.3.7 registry+https://github.com/rust-lang/crates.io-index",
"simd_helpers 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"siphasher 0.3.11 registry+https://github.com/rust-lang/crates.io-index",
"siphasher 1.0.1 registry+https://github.com/rust-lang/crates.io-index",
"slab 0.4.9 registry+https://github.com/rust-lang/crates.io-index",
"smallvec 1.15.0 registry+https://github.com/rust-lang/crates.io-index",
"smol_str 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
"socket2 0.5.9 registry+https://github.com/rust-lang/crates.io-index",
"stable_deref_trait 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
"static_assertions 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"strsim 0.11.1 registry+https://github.com/rust-lang/crates.io-index",
"strum 0.27.1 registry+https://github.com/rust-lang/crates.io-index",
"strum_macros 0.27.1 registry+https://github.com/rust-lang/crates.io-index",
"syn 1.0.109 registry+https://github.com/rust-lang/crates.io-index",
"syn 2.0.100 registry+https://github.com/rust-lang/crates.io-index",
"synstructure 0.13.1 registry+https://github.com/rust-lang/crates.io-index",
"sysinfo 0.33.1 registry+https://github.com/rust-lang/crates.io-index",
"tempfile 3.19.1 registry+https://github.com/rust-lang/crates.io-index",
"terminal_size 0.4.2 registry+https://github.com/rust-lang/crates.io-index",
"textwrap 0.16.2 registry+https://github.com/rust-lang/crates.io-index",
"thiserror 1.0.69 registry+https://github.com/rust-lang/crates.io-index",
"thiserror 2.0.12 registry+https://github.com/rust-lang/crates.io-index",
"thiserror-impl 1.0.69 registry+https://github.com/rust-lang/crates.io-index",
"thiserror-impl 2.0.12 registry+https://github.com/rust-lang/crates.io-index",
"thread_local 1.1.8 registry+https://github.com/rust-lang/crates.io-index",
"tiff 0.6.1 registry+https://github.com/rust-lang/crates.io-index",
"tiff 0.9.1 registry+https://github.com/rust-lang/crates.io-index",
"time 0.3.41 registry+https://github.com/rust-lang/crates.io-index",
"time-core 0.1.4 registry+https://github.com/rust-lang/crates.io-index",
"tokio 1.44.2 registry+https://github.com/rust-lang/crates.io-index",
"tokio-native-tls 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
"tokio-util 0.7.14 registry+https://github.com/rust-lang/crates.io-index",
"toml 0.5.11 registry+https://github.com/rust-lang/crates.io-index",
"tower 0.5.2 registry+https://github.com/rust-lang/crates.io-index",
"tower-layer 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
"tower-service 0.3.3 registry+https://github.com/rust-lang/crates.io-index",
"tracing 0.1.41 registry+https://github.com/rust-lang/crates.io-index",
"tracing-appender 0.2.3 registry+https://github.com/rust-lang/crates.io-index",
"tracing-attributes 0.1.28 registry+https://github.com/rust-lang/crates.io-index",
"tracing-core 0.1.33 registry+https://github.com/rust-lang/crates.io-index",
"tracing-error 0.2.1 registry+https://github.com/rust-lang/crates.io-index",
"tracing-log 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"tracing-subscriber 0.3.19 registry+https://github.com/rust-lang/crates.io-index",
"try-lock 0.2.5 registry+https://github.com/rust-lang/crates.io-index",
"ttf-parser 0.25.1 registry+https://github.com/rust-lang/crates.io-index",
"typenum 1.18.0 registry+https://github.com/rust-lang/crates.io-index",
"tz-rs 0.7.0 registry+https://github.com/rust-lang/crates.io-index",
"uds_windows 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"unicase 2.8.1 registry+https://github.com/rust-lang/crates.io-index",
"unicode-ident 1.0.18 registry+https://github.com/rust-lang/crates.io-index",
"unicode-segmentation 1.12.0 registry+https://github.com/rust-lang/crates.io-index",
"unicode-width 0.1.14 registry+https://github.com/rust-lang/crates.io-index",
"unicode-width 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"unicode-xid 0.2.6 registry+https://github.com/rust-lang/crates.io-index",
"unsafe-libyaml 0.2.11 registry+https://github.com/rust-lang/crates.io-index",
"uom 0.36.0 registry+https://github.com/rust-lang/crates.io-index",
"url 2.5.4 registry+https://github.com/rust-lang/crates.io-index",
"utf16_iter 1.0.5 registry+https://github.com/rust-lang/crates.io-index",
"utf8_iter 1.0.4 registry+https://github.com/rust-lang/crates.io-index",
"utf8parse 0.2.2 registry+https://github.com/rust-lang/crates.io-index",
"vcpkg 0.2.15 registry+https://github.com/rust-lang/crates.io-index",
"version_check 0.9.5 registry+https://github.com/rust-lang/crates.io-index",
"walkdir 2.5.0 registry+https://github.com/rust-lang/crates.io-index",
"want 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
"web-time 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"webbrowser 1.0.4 registry+https://github.com/rust-lang/crates.io-index",
"weezl 0.1.8 registry+https://github.com/rust-lang/crates.io-index",
"which 7.0.3 registry+https://github.com/rust-lang/crates.io-index",
"winapi 0.3.9 registry+https://github.com/rust-lang/crates.io-index",
"winapi-util 0.1.9 registry+https://github.com/rust-lang/crates.io-index",
"windows 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
"windows 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
"windows 0.60.0 registry+https://github.com/rust-lang/crates.io-index",
"windows 0.61.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-collections 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-collections 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-core 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-core 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-core 0.60.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-core 0.61.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-future 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-future 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-icons 0.1.0 git+https://github.com/LGUG2Z/windows-icons?rev=0c9d7ee1b807347c507d3a9862dd007b4d3f4354",
"windows-icons 0.1.0 git+https://github.com/LGUG2Z/windows-icons?rev=d67cc9920aa9b4883393e411fb4fa2ddd4c498b5",
"windows-implement 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-implement 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-implement 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-implement 0.60.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-interface 0.57.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-interface 0.58.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-interface 0.59.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-link 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-numerics 0.1.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-numerics 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-registry 0.4.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-result 0.1.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-result 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-result 0.3.2 registry+https://github.com/rust-lang/crates.io-index",
"windows-strings 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-strings 0.3.1 registry+https://github.com/rust-lang/crates.io-index",
"windows-strings 0.4.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-sys 0.48.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-sys 0.52.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-sys 0.59.0 registry+https://github.com/rust-lang/crates.io-index",
"windows-targets 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
"windows-targets 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
"windows-targets 0.53.0 registry+https://github.com/rust-lang/crates.io-index",
"windows_aarch64_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
"windows_aarch64_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
"windows_aarch64_msvc 0.53.0 registry+https://github.com/rust-lang/crates.io-index",
"windows_i686_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
"windows_i686_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
"windows_i686_msvc 0.53.0 registry+https://github.com/rust-lang/crates.io-index",
"windows_x86_64_msvc 0.48.5 registry+https://github.com/rust-lang/crates.io-index",
"windows_x86_64_msvc 0.52.6 registry+https://github.com/rust-lang/crates.io-index",
"windows_x86_64_msvc 0.53.0 registry+https://github.com/rust-lang/crates.io-index",
"winput 0.2.5 registry+https://github.com/rust-lang/crates.io-index",
"winreg 0.55.0 registry+https://github.com/rust-lang/crates.io-index",
"winsafe 0.0.19 registry+https://github.com/rust-lang/crates.io-index",
"wmi 0.15.2 registry+https://github.com/rust-lang/crates.io-index",
"write16 1.0.0 registry+https://github.com/rust-lang/crates.io-index",
"xml-rs 0.8.26 registry+https://github.com/rust-lang/crates.io-index",
"yaml-rust 0.4.5 registry+https://github.com/rust-lang/crates.io-index",
"zerocopy 0.7.35 registry+https://github.com/rust-lang/crates.io-index",
"zerocopy 0.8.24 registry+https://github.com/rust-lang/crates.io-index",
"zune-core 0.4.12 registry+https://github.com/rust-lang/crates.io-index",
"zune-inflate 0.2.54 registry+https://github.com/rust-lang/crates.io-index",
"zune-jpeg 0.4.14 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"MIT-0",
[
"dunce 1.0.5 registry+https://github.com/rust-lang/crates.io-index",
"tzdb_data 0.2.1 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"MPL-2.0",
[
"option-ext 0.2.0 registry+https://github.com/rust-lang/crates.io-index",
"ramhorns 1.0.1 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"OFL-1.1",
[
"epaint_default_fonts 0.31.1 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"Ubuntu-font-1.0",
[
"epaint_default_fonts 0.31.1 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"Unicode-3.0",
[
"icu_collections 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
"icu_locid 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
"icu_locid_transform 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
"icu_locid_transform_data 1.5.1 registry+https://github.com/rust-lang/crates.io-index",
"icu_normalizer 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
"icu_normalizer_data 1.5.1 registry+https://github.com/rust-lang/crates.io-index",
"icu_properties 1.5.1 registry+https://github.com/rust-lang/crates.io-index",
"icu_properties_data 1.5.1 registry+https://github.com/rust-lang/crates.io-index",
"icu_provider 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
"icu_provider_macros 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
"litemap 0.7.5 registry+https://github.com/rust-lang/crates.io-index",
"tinystr 0.7.6 registry+https://github.com/rust-lang/crates.io-index",
"unicode-ident 1.0.18 registry+https://github.com/rust-lang/crates.io-index",
"writeable 0.5.5 registry+https://github.com/rust-lang/crates.io-index",
"yoke 0.7.5 registry+https://github.com/rust-lang/crates.io-index",
"yoke-derive 0.7.5 registry+https://github.com/rust-lang/crates.io-index",
"zerofrom 0.1.6 registry+https://github.com/rust-lang/crates.io-index",
"zerofrom-derive 0.1.6 registry+https://github.com/rust-lang/crates.io-index",
"zerovec 0.10.4 registry+https://github.com/rust-lang/crates.io-index",
"zerovec-derive 0.10.3 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"Unlicense",
[
"aho-corasick 1.1.3 registry+https://github.com/rust-lang/crates.io-index",
"byteorder 1.5.0 registry+https://github.com/rust-lang/crates.io-index",
"byteorder-lite 0.1.0 registry+https://github.com/rust-lang/crates.io-index",
"memchr 2.7.4 registry+https://github.com/rust-lang/crates.io-index",
"regex-automata 0.1.10 registry+https://github.com/rust-lang/crates.io-index",
"same-file 1.0.6 registry+https://github.com/rust-lang/crates.io-index",
"walkdir 2.5.0 registry+https://github.com/rust-lang/crates.io-index",
"winapi-util 0.1.9 registry+https://github.com/rust-lang/crates.io-index"
]
],
[
"Zlib",
[
"adler32 1.2.0 registry+https://github.com/rust-lang/crates.io-index",
"bytemuck 1.22.0 registry+https://github.com/rust-lang/crates.io-index",
"bytemuck_derive 1.9.3 registry+https://github.com/rust-lang/crates.io-index",
"const_format 0.2.34 registry+https://github.com/rust-lang/crates.io-index",
"const_format_proc_macros 0.2.34 registry+https://github.com/rust-lang/crates.io-index",
"cursor-icon 1.1.0 registry+https://github.com/rust-lang/crates.io-index",
"foldhash 0.1.5 registry+https://github.com/rust-lang/crates.io-index",
"glow 0.16.0 registry+https://github.com/rust-lang/crates.io-index",
"miniz_oxide 0.4.4 registry+https://github.com/rust-lang/crates.io-index",
"miniz_oxide 0.8.8 registry+https://github.com/rust-lang/crates.io-index",
"raw-window-handle 0.6.2 registry+https://github.com/rust-lang/crates.io-index",
"zune-core 0.4.12 registry+https://github.com/rust-lang/crates.io-index",
"zune-inflate 0.2.54 registry+https://github.com/rust-lang/crates.io-index",
"zune-jpeg 0.4.14 registry+https://github.com/rust-lang/crates.io-index"
]
]
]
}

View File

@@ -10,9 +10,8 @@ Options:
Desired ease function for animation
[default: linear]
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart,
ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint, ease-in-expo, ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ,
ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
[possible values: linear, ease-in-sine, ease-out-sine, ease-in-out-sine, ease-in-quad, ease-out-quad, ease-in-out-quad, ease-in-cubic, ease-in-out-cubic, ease-in-quart, ease-out-quart, ease-in-out-quart, ease-in-quint, ease-out-quint, ease-in-out-quint,
ease-in-expo, ease-out-expo, ease-in-out-expo, ease-in-circ, ease-out-circ, ease-in-out-circ, ease-in-back, ease-out-back, ease-in-out-back, ease-in-elastic, ease-out-elastic, ease-in-out-elastic, ease-in-bounce, ease-out-bounce, ease-in-out-bounce]
-a, --animation-type <ANIMATION_TYPE>
Animation type to apply the style to. If not specified, sets global style

View File

@@ -18,7 +18,7 @@ Arguments:
Options:
-w, --window-kind <WINDOW_KIND>
[default: single]
[possible values: single, stack, monocle, unfocused, floating]
[possible values: single, stack, monocle, unfocused, unfocused-locked, floating]
-h, --help
Print help

View File

@@ -0,0 +1,12 @@
# clear-session-float-rules
```
Clear all session float rules
Usage: komorebic.exe clear-session-float-rules
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,16 @@
# cycle-empty-workspace
```
Focus the next empty workspace in the given cycle direction (if one exists)
Usage: komorebic.exe cycle-empty-workspace <CYCLE_DIRECTION>
Arguments:
<CYCLE_DIRECTION>
[possible values: previous, next]
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,12 @@
# data-directory
```
Show the path to komorebi's data directory in %LOCALAPPDATA%
Usage: komorebic.exe data-directory
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,12 @@
# move-to-last-workspace
```
Move the focused window to the last focused monitor workspace
Usage: komorebic.exe move-to-last-workspace
Options:
-h, --help
Print help
```

View File

@@ -7,7 +7,7 @@ Usage: komorebic.exe query <STATE_QUERY>
Arguments:
<STATE_QUERY>
[possible values: focused-monitor-index, focused-workspace-index, focused-container-index, focused-window-index, focused-workspace-name]
[possible values: focused-monitor-index, focused-workspace-index, focused-container-index, focused-window-index, focused-workspace-name, focused-workspace-layout, version]
Options:
-h, --help

View File

@@ -0,0 +1,12 @@
# send-to-last-workspace
```
Send the focused window to the last focused monitor workspace
Usage: komorebic.exe send-to-last-workspace
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,12 @@
# session-float-rule
```
Add a rule to float the foreground window for the rest of this session
Usage: komorebic.exe session-float-rule
Options:
-h, --help
Print help
```

View File

@@ -0,0 +1,12 @@
# session-float-rules
```
Show all session float rules
Usage: komorebic.exe session-float-rules
Options:
-h, --help
Print help
```

12
docs/cli/toggle-lock.md Normal file
View File

@@ -0,0 +1,12 @@
# toggle-lock
```
Toggle a lock for the focused container, ensuring it will not be displaced by any new windows
Usage: komorebic.exe toggle-lock
Options:
-h, --help
Print help
```

View File

@@ -1,8 +1,7 @@
# toggle-workspace-float-override
```
Enable or disable float override, which makes it so every new window opens in floating mode, for the currently focused workspace. If there was no override value set for the workspace
previously it takes the opposite of the global value
Enable or disable float override, which makes it so every new window opens in floating mode, for the currently focused workspace. If there was no override value set for the workspace previously it takes the opposite of the global value
Usage: komorebic.exe toggle-workspace-float-override

View File

@@ -1,8 +1,7 @@
# toggle-workspace-window-container-behaviour
```
Toggle the behaviour for new windows (stacking or dynamic tiling) for currently focused workspace. If there was no behaviour set for the workspace previously it takes the opposite of the
global value
Toggle the behaviour for new windows (stacking or dynamic tiling) for currently focused workspace. If there was no behaviour set for the workspace previously it takes the opposite of the global value
Usage: komorebic.exe toggle-workspace-window-container-behaviour

View File

@@ -0,0 +1,412 @@
# Multi-Monitor Setup
You can set up komorebi to work with multiple monitors. To do so, first you start by setting up multiple monitor
configurations on your `komorebi.json` config file.
If you've used the [`komorebic quickstart`](../cli/quickstart.md) command you'll already have a `komorebi.json` config
file with one monitor config setup. Open this file and look for the `"monitors":` line, you should find something like
this:
```json
{
"monitors": [
{
"workspaces": [
{
"name": "I",
"layout": "BSP"
},
{
"name": "II",
"layout": "VerticalStack"
},
{
"name": "III",
"layout": "HorizontalStack"
},
{
"name": "IV",
"layout": "UltrawideVerticalStack"
},
{
"name": "V",
"layout": "Rows"
},
{
"name": "VI",
"layout": "Grid"
},
{
"name": "VII",
"layout": "RightMainVerticalStack"
}
]
}
]
}
```
For this example we will remove some workspaces to simplify the config so it is easier to look at, but feel free to
set up as many workspaces per monitor as you'd like. Here is the same configuration with only 3 workspaces.
```json
{
"monitors": [
{
"workspaces": [
{
"name": "I",
"layout": "BSP"
},
{
"name": "II",
"layout": "VerticalStack"
},
{
"name": "III",
"layout": "HorizontalStack"
}
]
}
]
}
```
Let's add another monitor:
```json
{
"monitors": [
// monitor 1, index 0
{
"workspaces": [
{
"name": "I",
"layout": "BSP"
},
{
"name": "II",
"layout": "VerticalStack"
},
{
"name": "III",
"layout": "HorizontalStack"
}
]
},
// monitor 2, index 1
{
"workspaces": [
{
"name": "1",
"layout": "BSP"
},
{
"name": "2",
"layout": "VerticalStack"
},
{
"name": "3",
"layout": "HorizontalStack"
}
]
}
]
}
```
Now have two monitor configurations. We have the first monitor configuration, which is index 0 (*usually
on programming languages the first item of a list starts with index 0*), this configuration has 3 workspaces with names
"I", "II" and "III". Then the 2nd monitor configuration, which is index 1, also has 3 workspaces with names "1", "2",
and "3" (you should always give unique names to your workspaces).
Now if you start komorebi with two monitors connected, the main monitor will use the configuration with index 0 and the
secondary monitor will use the configuration with index 1.
---
Let's say you have more monitors, or you want to make sure that a certain configuration is always applied to a certain
monitor. For this you will want to use the `display_index_preferences`.
Open up a terminal and type the following command: [ `komorebic monitor-info`](../cli/monitor-information.md). This
command will give you the information about your connected monitors, you want to look up the `serial_number_id`. You
should get something like this:
```
komorebic monitor-info
[
{
"id": 6620935,
"name": "DISPLAY1",
"device": "BOE0A1C",
"device_id": "BOE0A1C-5&a2bea0b&0&UID512",
"serial_number_id": "0",
"size": {
"left": 0,
"top": 0,
"right": 1920,
"bottom": 1080
}
},
{
"id": 181932057,
"name": "DISPLAY2",
"device": "VSC8C31",
"device_id": "VSC8C31-5&18560b1f&0&UID4356",
"serial_number_id": "UEP174021562",
"size": {
"left": 0,
"top": -1080,
"right": 1920,
"bottom": 1080
}
}
]
```
In this case the setup is a laptop with a secondary monitor connected. You'll need to figure out which monitor is which,
usually the display name's number should be similar to the numbers you can find on
`Windows Settings -> System -> Display`.
If you have trouble with this step you can always jump on Discord and ask for help (create a `Support` thread).
Once you know which monitor is which, you want to look up their `serial_number_id` to use that on
`display_index_preferences`, you can also use the `device_id`, it accepts both however there have been reported cases
where the `device_id` changes after a restart while the `serial_number_id` doesn't.
So with the example above, we want the laptop to always use the configuration index 0 and the other monitor to use
configuration index 1, so we map the configuration index number to the monitor `serial_number_id`/`device_id` like this:
```json
{
"display_index_preferences": {
"0": "0",
"1": "UEP174021562"
}
}
```
Again you could also have used the `device_id` like this:
```json
{
"display_index_preferences": {
"0": "BOE0A1C-5&a2bea0b&0&UID512",
"1": "VSC8C31-5&18560b1f&0&UID4356"
}
}
```
You should add this `display_index_preferences` option to your `komorebi.json` file. If you find that something is
not working as expected you can try to use the command `komorebic check`.
> [!IMPORTANT]
>
> **When using multiple monitors it is recommended to always set the `display_index_preferences`. If you don't you might
get some undefined behaviour.**
---
If you would like to run multiple instances of `komorebi-bar` to target different monitors, it is possible to do so
using the `bar_configurations` array in your `komorebi.json` configuration file. You can refer to the
[multiple-bar-instances](multiple-bar-instances.md) documentation.
In this case it is important to use `display_index_preferences`, because if you don't, and you have 3 or more monitors,
disconnecting and reconnecting monitors may result in the bars for the monitors getting shifted around.
Consider this setup with 3 monitors (A, B and C):
```json
// HOME_MONITOR_1_BAR.json
{
"monitor_index": 0
//...
}
```
```json
// HOME_MONITOR_2_BAR.json
{
"monitor_index": 1
//...
}
```
```json
// WORK_MONITOR_1_BAR.json
{
"monitor_index": 2
//...
}
```
```json
{
"display_index_preferences": {
"0": "MONITOR_1_ID",
"1": "MONITOR_2_ID",
"2": "MONITOR_3_ID"
},
"bar_configurations": [
// this bar uses "monitor_index": 0,
"path/to/bar_config_1.json",
// this bar uses "monitor_index": 1,
"path/to/bar_config_2.json",
// this bar uses "monitor_index": 2,
"path/to/bar_config_3.json"
]
}
```
Komorebi uses an internal map to keep track of monitor to config indices, this map is called `monitor_usr_idx_map` it is
an internal variable to komorebi that you don't need to do anything with, but you can see it with the [
`komorebic state`](../cli/state.md) command (in case you need to debug something).
At first, komorebi will load all monitors and set the internal index map (`monitor_usr_idx_map`) as:
```json
{
// This is monitor A
"0": 0,
// This is monitor B
"1": 1,
// This is monitor C
"2": 2
}
```
Which kind of seems unnecessary, but imagine that then you disconnect monitor B (or it goes to sleep). Then komorebi
will only have 2 monitors with index 0 and 1, so the above map will be updated to this:
```jsonc
[
"0": 0, // This is monitor A
"2": 1, // This is now monitor C, because monitor B disconnected
]
```
So now the bar intended to be for monitor B, which was looking for index "1" on that map, doesn't see it and knows it
should be disabled. And the bar for monitor C looks at that map and knows that it's index "2" now maps to index 1 so it
uses that index internally to get all the correct values about the monitor.
If you didn't have the `display_index_preferences` set, then when you disconnected monitor B, komorebi wouldn't know
how to map the indices and would use default behaviour which would result in a map like this:
```json
{
// This is monitor A
"0": 0,
// This is monitor C, because monitor B disconnected. However the bars will think it is monitor B because it has index "1"
"1": 1
}
```
# Multiple Monitors on different machines
You can use the same `komorebi.json` to configure two different setups and then synchronize your config across machines.
However, if you do this it is important to be aware of a few things.
Firstly, using `display_index_preferences` is required in this case.
You will need to get the `serial_number_id` or `device_id` of all the monitors of all your setups. With that information
you would then set your config like this:
```json
{
"display_index_preferences": {
"0": "HOME_MONITOR_1_ID",
"1": "HOME_MONITOR_2_ID",
"2": "WORK_MONITOR_1_ID",
"3": "WORK_MONITOR_2_ID"
},
"monitors": [
// HOME_MONITOR_1
{
"workspaces": [
// ...
]
},
// HOME_MONITOR_2
{
"workspaces": [
// ...
]
},
// WORK_MONITOR_1
{
"workspaces": [
// ...
]
},
// WORK_MONITOR_2
{
"workspaces": [
// ...
]
}
]
}
```
> [!NOTE]
>
> *You can't use the same config on two different monitors, you have to make a duplicated config for each monitor!*
Then on the bar configs you need to set the bar's monitor index like this:
```json
// HOME_MONITOR_1_BAR.json
{
"monitor_index": 0
//...
}
```
```json
// HOME_MONITOR_2_BAR.json
{
"monitor_index": 1
//...
}
```
```json
// WORK_MONITOR_1_BAR.json
{
"monitor_index": 2
//...
}
```
```json
// WORK_MONITOR_2_BAR.json
{
"monitor_index": 3
//...
}
```
Although you will only ever have 2 monitors connected at any one time, and they'll always have index 0 and 1, the
above config will still work on both physical configurations.
This is because komorebi will apply the appropriate config to the loaded monitors and will create a map of the user
index (the index defined in the user config) to the actual monitor index, and the bar will use that map to know if it
should be enabled, and where it should be drawn.
### Things to keep in mind
* If you are using a laptop connected to one monitor at work and a different one at home, the work monitor and the home
monitor are considered different monitors by komorebi
* When you disconnect from work, komorebi will keep the work monitor cached
* You can still use a laptop alone without any monitor and if you need a window that was on the other monitor you can
press the taskbar icon or use `alt + tab` to bring it to focus and that window will now be part of the laptop monitor
* If you then reconnect the work monitor, the cached version will be applied with all its windows (except any window(s)
you might have moved to another monitor)
* If however, instead of reconnecting the work monitor, you connect the home monitor, then the work monitor will still
remain cached, and komorebi will load the home monitor from the cache (if it exists)
* Sometimes when you disconnect/reconnect a monitor the event might be missed by komorebi, meaning that Windows will
show you both monitors but komorebi won't know about the existence of one of them
* If you notice this type of weird behaviour, always run the [
`komorebic monitor-info`](../cli/monitor-information.md)
command and validate if one of the monitors is missing
* To fix this you can try disconnecting and reconnecting the monitor again, or restarting komorebi

View File

@@ -8,12 +8,8 @@ configuration file.
```json
{
"default_workspace_padding": 0,
"default_container_padding": 0,
"border_width": 0,
"border_offset": -1
"default_container_padding": -1,
}
```
A restart of `komorebi` is required after changing these settings.
[![Watch the tutorial video](https://img.youtube.com/vi/6QYLao953XE/hqdefault.jpg)](https://www.youtube.com/watch?v=6QYLao953XE)

View File

@@ -34,7 +34,7 @@ showcases the many awesome projects that exist in the `komorebi` ecosystem.
## Licensing for Personal Use
`komorebi` is licensed under the [Komorebi 1.0.0 license](https://github.com/LGUG2Z/komorebi-license), which is a fork
`komorebi` is licensed under the [Komorebi 2.0.0 license](https://github.com/LGUG2Z/komorebi-license), which is a fork
of the [PolyForm Strict 1.0.0 license](https://polyformproject.org/licenses/strict/1.0.0). On a high level this means
that you are free to do whatever you want with `komorebi` for personal use other than redistribution, or distribution of
new works (i.e. hard-forks) based on the software.
@@ -42,7 +42,7 @@ new works (i.e. hard-forks) based on the software.
Anyone is free to make their own fork of `komorebi` with changes intended either for personal use or for integration
back upstream via pull requests.
The [Komorebi 1.0.0 License](https://github.com/LGUG2Z/komorebi-license) does not permit any kind of commercial use (
The [Komorebi 2.0.0 License](https://github.com/LGUG2Z/komorebi-license) does not permit any kind of commercial use (
i.e. using `komorebi` at work).
## Sponsorship for Personal Use

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.34/schema.bar.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.36/schema.bar.json",
"monitor": 0,
"font_family": "JetBrains Mono",
"theme": {

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.34/schema.json",
"$schema": "https://raw.githubusercontent.com/LGUG2Z/komorebi/v0.1.36/schema.json",
"app_specific_configuration_path": "$Env:USERPROFILE/applications.json",
"window_hiding_behaviour": "Cloak",
"cross_monitor_move_behaviour": "Insert",
@@ -14,13 +14,6 @@
"unfocused_border": "Base03",
"bar_accent": "Base0D"
},
"stackbar": {
"height": 40,
"mode": "OnStack",
"tabs": {
"width": 300
}
},
"monitors": [
{
"workspaces": [

View File

@@ -132,3 +132,19 @@ running `komorebic stop` and `komorebic start`.
We can see the _komorebi_ state is no longer associated with the previous
device: `null`, suggesting an issue when the display resumes from a suspended
state.
## Komorebi Bar does not render transparency on Nvidia GPUs
Users with Nvidia GPUs may have issues with transparency on the Komorebi Bar.
To solve this the user can do the following:
1. Open the Nvidia Control Panel
2. On the left menu tree, under "3D Settings", select "Manage 3D Settings"
3. Select the "Program Settings" tab
4. Press the "Add" button and select "komorebi-bar"
5. Under "3. Specify the settings for this program:", find the feature labelled, "OpenGL GDI compatibility"
6. Change the setting to "Prefer compatibility"
7. At the bottom of the window select "Apply"
8. Restart the Komorebi Bar with "komorebic stop --bar; komorebic start --bar"
This should resolve the issue and your Komorebi Bar should render with the proper transparency.

View File

@@ -0,0 +1,46 @@
# Focusing Windows
Windows can be focused in a direction (left, down, up, right) using the [`komorebic focus`](../cli/focus.md) command.
```
# example showing how you might bind this command
alt + h : komorebic focus left
alt + j : komorebic focus down
alt + k : komorebic focus up
alt + l : komorebic focus right
```
Windows can be focused in a cycle direction (previous, next) using the [`komorebic cycle-focus`](../cli/cycle-focus.md)
command.
```
# example showing you might bind this command
alt + shift + oem_4 : komorebic cycle-focus previous # oem_4 is [
alt + shift + oem_6 : komorebic cycle-focus next # oem_6 is ]
```
It is possible to attempt to focus the first window, on any workspace, matching an exe using the [
`komorebic eager-focus`](../cli/eager-focus.md) command.
```
# example showing how you might bind this command
win + 1 : komorebic eager-focus firefox.exe
```
The window at the largest tile can be focused using the [`komorebic promote-focus`](../cli/promote-focus.md) command.
```
# example showing how you might bind this command
alt + return : komorebic promote-focus
```
The behaviour when attempting to call `komorebic focus` when at the left or right edge of a monitor is determined by
the [`cross_boundary_behaviour`](https://komorebi.lgug2z.com/schema#cross_boundary_behaviour) configuration option.
When set to `Workspace`, the next workspace on the same monitor will be focused.
When set to `Monitor`, the focused workspace on the next monitor in the given direction will be focused.

View File

@@ -0,0 +1,59 @@
# Focusing Workspaces
Workspaces on the focused monitor can be focused by their index using the [
`komorebic focus-workspace`](../cli/focus-workspace.md) command.
If this command is called with an index for a workspace which does not exist, that workspace, and all workspace indexes
required to get to that workspace, will be created.
```
# example showing how you might bind this command
alt + 1 : komorebic focus-workspace 0
alt + 2 : komorebic focus-workspace 1
alt + 3 : komorebic focus-workspace 2
```
Workspaces on the focused monitor can be focused in a cycle direction (previous, next) using the [
`komorebic cycle-workspace`](../cli/cycle-workspace.md) command.
```
# example showing how you might bind this command
alt + shift + oem_4 : komorebic cycle-workspace previous # oem_4 is [
alt + shift + oem_6 : komorebic cycle-workspace next # oem_6 is ]
```
Workspaces on other monitors can be focused by both the monitor index and the workspace index using the [
`komorebic focus-monitor-workspace`](../cli/focus-monitor-workspace.md) command.
```
# example showing how you might bind this command
alt + 1 : komorebic focus-monitor-workspace 0 0
alt + 2 : komorebic focus-monitor-workspace 0 1
alt + 3 : komorebic focus-monitor-workspace 1 0
```
Workspaces on any monitor can be focused by their name (given that all workspace names across all monitors are unique)
using the [`komorebic focus-named-workspace`](../cli/focus-named-workspace.md) command.
```
# example showing how you might bind this command
alt + c : komorebic focus-named-workspace coding
```
Workspaces on all monitors can be set to the same index (emulating single workspaces which span across all monitors)
using the [`komorebic focus-workspaces`](../cli/focus-workspaces.md) command.
```
# example showing how you might bind this command
alt + 1 : komorebic focus-workspaces 0
alt + 2 : komorebic focus-workspaces 1
alt + 3 : komorebic focus-workspaces 2
```
The last focused workspace on the focused monitor can be re-focused using the [
`komorebic focus-last-workspace`](../cli/focus-last-workspace.md) command.

View File

@@ -0,0 +1,59 @@
# Moving Windows Across Workspaces
Windows can be moved to another workspace on the focused monitor using the [
`komorebic move-to-workspace`](../cli/move-to-workspace.md) command. This command will also move your focus to the
target workspace.
```
# example showing how you might bind this command
alt + shift + 1 : komorebic move-to-workspace 0
alt + shift + 2 : komorebic move-to-workspace 1
alt + shift + 3 : komorebic move-to-workspace 2
```
Windows can be sent to another workspace on the focused monitor using the [
`komorebic send-to-workspace`](../cli/send-to-workspace.md) command. This command will keep your focus on the origin
workspace.
```
# example showing how you might bind this command
alt + shift + 1 : komorebic send-to-workspace 0
alt + shift + 2 : komorebic send-to-workspace 1
alt + shift + 3 : komorebic send-to-workspace 2
```
Windows can be moved to another workspace on the focused monitor in a cycle direction (previous, next) using the [
`komorebic cycle-move-to-workspace`](../cli/cycle-move-to-workspace.md) command. This command will also move your focus
to the target workspace.
```
# example showing how you might bind this command
alt + shift + oem_4 : komorebic cycle-move-to-workspace previous # oem_4 is [
alt + shift + oem_6 : komorebic cycle-move-to-workspace next # oem_6 is ]
```
Windows can be sent to another workspace on the focused monitor in a cycle direction (previous, next) using the [
`komorebic cycle-move-to-workspace`](../cli/cycle-move-to-workspace.md) command. This command will keep your focus on
the origin workspace.
```
# example showing how you might bind this command
alt + shift + oem_4 : komorebic cycle-send-to-workspace previous # oem_4 is [
alt + shift + oem_6 : komorebic cycle-send-to-workspace next # oem_6 is ]
```
Windows can be moved or sent to the focused workspace on a another monitor using the [
`komorebic move-to-monitor`](../cli/move-to-monitor.md) and [`komorebic send-to-monitor`](../cli/send-to-monitor.md)
commands.
Windows can be moved or sent to the focused workspace on a monitor in a cycle direction (previous, next) using the [
`komorebic cycle-move-to-monitor`](../cli/cycle-move-to-monitor.md) and [
`komorebic cycle-send-to-monitor`](../cli/cycle-send-to-monitor.md) commands.
Windows can be moved or sent to a named workspace on any monitor (given that all workspace names across all monitors are
unique) using the [`komorebic move-to-named-workspace`](../cli/move-to-named-workspace.md) and [
`komorebic send-to-named-workspace`](../cli/send-to-named-workspace.md) commands

View File

@@ -0,0 +1,50 @@
# Moving Windows
Windows can be moved in a direction (left, down, up, right) using the [`komorebic move`](../cli/move.md) command.
```
# example showing how you might bind this command
alt + shift + h : komorebic move left
alt + shift + j : komorebic move down
alt + shift + k : komorebic move up
alt + shift + l : komorebic move right
```
Windows can be moved in a cycle direction (previous, next) using the [`komorebic cycle-move`](../cli/cycle-move.md)
command.
```
# example showing how you might bind this command
alt + shift + oem_4 : komorebic cycle-move previous # oem_4 is [
alt + shift + oem_6 : komorebic cycle-move next # oem_6 is ]
```
The focused window can be moved to the largest tile using the [`komorebic promote`](../cli/promote.md) command.
```
# example showing how you might bind this command
alt + shift + return : komorebic promote
```
The behaviour when attempting to call `komorebic move` when at the left or right edge of a monitor is determined by
the [`cross_boundary_behaviour`](https://komorebi.lgug2z.com/schema#cross_boundary_behaviour) configuration option.
When set to `Workspace`, the focused window will be moved to the next workspace on the focused monitor in the given
direction
When set to `Monitor`, the focused window will be moved to the focused workspace on the next monitor in the given
direction.
The behaviour when calling `komorebic move` with `cross_boundary_behaviour` set to `Monitor` can be further refined with
the [`cross_monitor_move_behaviour`](https://komorebi.lgug2z.com/schema#cross_monitor_move_behaviour) configuration
option.
When set to `Swap`, the focused window will be swapped with the window at the corresponding edge of the adjacent monitor
When set to `Insert`, the focused window will be inserted into the focused workspace on the adjacent monitor.
When set to `NoOp`, the focused window will not be moved across a monitor boundary, though focusing across monitor
boundaries will continue to function.

View File

@@ -0,0 +1,52 @@
# Stacking Windows
Windows can be stacked in a direction (left, down, up, right) using the [`komorebic stack`](../cli/stack.md) command.
```
# example showing how you might bind this command
alt + left : komorebic stack left
alt + down : komorebic stack down
alt + up : komorebic stack up
alt + right : komorebic stack right
```
Windows can be popped from a stack using the [`komorebic unstack`](../cli/unstack.md) command.
```
# example showing how you might bind this command
alt + oem_1 : komorebic unstack # oem_1 is ;
```
Windows in a stack can be focused in a cycle direction (previous, next) using the [
`komorebic cycle-stack`](../cli/cycle-stack.md) command.
```
# example showing how you might bind this command
alt + oem_4 : komorebic cycle-stack previous # oem_4 is [
alt + oem_6 : komorebic cycle-stack next # oem_6 is ]
```
Windows in a stack can have their positions in the stack moved in a cycle direction (previous, next) using the [
`komorebic cycle-stack-index`](../cli/cycle-stack-index.md) command.
```
# example showing how you might bind this command
alt + shift + oem_4 : komorebic cycle-stack-index previous # oem_4 is [
alt + shift + oem_6 : komorebic cycle-stack-index next # oem_6 is ]
```
Windows in a stack can be focused by their index in the stack using the [
`komorebic focus-stack-window`](../cli/focus-stack-window.md) command.
All windows on the focused workspace can be combined into a single stack using the [
`komorebic stack-all`](../cli/stack-all.md) command.
All windows in a focused stack can be popped using the [`komorebic unstack-all`](../cli/unstack-all.md) command.
It is possible to tell the window manager to stack the next opened window on top of the currently focused window by
using the [
`komorebic toggle-workspace-window-container-behaviour`](../cli/toggle-workspace-window-container-behaviour.md) command.

View File

@@ -19,22 +19,31 @@ install-targets *targets:
"{{ targets }}" -split ' ' | ForEach-Object { just install-target $_ }
install-target target:
cargo +stable install --path {{ target }} --locked --no-default-features
install-targets-with-jsonschema *targets:
"{{ targets }}" -split ' ' | ForEach-Object { just install-target-with-jsonschema $_ }
install-target-with-jsonschema target:
cargo +stable install --path {{ target }} --locked
install:
just install-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
install-with-jsonschema:
just install-targets-with-jsonschema komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
build-targets *targets:
"{{ targets }}" -split ' ' | ForEach-Object { just build-target $_ }
build-target target:
cargo +stable build --package {{ target }} --locked --profile release-jeezy
cargo +stable build --package {{ target }} --locked --release --no-default-features
build:
just build-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
copy-target target:
cp .\target\release-jeezy\{{ target }}.exe $Env:USERPROFILE\.cargo\bin
cp .\target\release\{{ target }}.exe $Env:USERPROFILE\.cargo\bin
copy-targets *targets:
"{{ targets }}" -split ' ' | ForEach-Object { just copy-target $_ }
@@ -46,7 +55,7 @@ copy:
just copy-targets komorebic komorebic-no-console komorebi komorebi-bar komorebi-gui
run target:
cargo +stable run --bin {{ target }} --locked
cargo +stable run --bin {{ target }} --locked --no-default-features
warn target $RUST_LOG="warn":
just run {{ target }}
@@ -61,7 +70,7 @@ trace target $RUST_LOG="trace":
just run {{ target }}
deadlock $RUST_LOG="trace":
cargo +stable run --bin komorebi --locked --features deadlock_detection
cargo +stable run --bin komorebi --locked --no-default-features --features deadlock_detection
docgen:
cargo run --package komorebic -- docgen

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-bar"
version = "0.1.35"
version = "0.1.36"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -9,6 +9,7 @@ edition = "2021"
komorebi-client = { path = "../komorebi-client" }
komorebi-themes = { path = "../komorebi-themes" }
chrono-tz = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true }
color-eyre = { workspace = true }
@@ -20,13 +21,14 @@ egui-phosphor = "0.9"
font-loader = "0.11"
hotwatch = { workspace = true }
image = "0.25"
netdev = "0.32"
lazy_static = { workspace = true }
netdev = "0.33"
num = "0.4"
num-derive = "0.4"
num-traits = "0.2"
random_word = { version = "0.4", features = ["en"] }
random_word = { version = "0.5", features = ["en"] }
reqwest = { version = "0.12", features = ["blocking"] }
schemars = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true }
serde_json = { workspace = true }
starship-battery = "0.10"
@@ -35,4 +37,9 @@ tracing = { workspace = true }
tracing-subscriber = { workspace = true }
windows = { workspace = true }
windows-core = { workspace = true }
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }
windows-icons = { git = "https://github.com/LGUG2Z/windows-icons", rev = "0c9d7ee1b807347c507d3a9862dd007b4d3f4354" }
windows-icons-fallback = { package = "windows-icons", git = "https://github.com/LGUG2Z/windows-icons", rev = "d67cc9920aa9b4883393e411fb4fa2ddd4c498b5" }
[features]
default = ["schemars"]
schemars = ["dep:schemars"]

View File

@@ -4,16 +4,18 @@ use crate::config::KomobarTheme;
use crate::config::MonitorConfigOrIndex;
use crate::config::Position;
use crate::config::PositionConfig;
use crate::komorebi::Komorebi;
use crate::komorebi::KomorebiNotificationState;
use crate::process_hwnd;
use crate::render::Color32Ext;
use crate::render::Grouping;
use crate::render::RenderConfig;
use crate::render::RenderExt;
use crate::widget::BarWidget;
use crate::widget::WidgetConfig;
use crate::widgets::komorebi::Komorebi;
use crate::widgets::komorebi::KomorebiNotificationState;
use crate::widgets::widget::BarWidget;
use crate::widgets::widget::WidgetConfig;
use crate::KomorebiEvent;
use crate::AUTO_SELECT_FILL_COLOUR;
use crate::AUTO_SELECT_TEXT_COLOUR;
use crate::BAR_HEIGHT;
use crate::DEFAULT_PADDING;
use crate::MAX_LABEL_WIDTH;
@@ -43,12 +45,15 @@ use eframe::egui::Vec2;
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_themes::catppuccin_egui;
use komorebi_themes::Base16Value;
use komorebi_themes::Base16Wrapper;
use komorebi_themes::Catppuccin;
use komorebi_themes::CatppuccinValue;
use std::cell::RefCell;
@@ -87,75 +92,86 @@ pub fn apply_theme(
grouping: Option<Grouping>,
render_config: Rc<RefCell<RenderConfig>>,
) {
match theme {
let (auto_select_fill, auto_select_text) = match theme {
KomobarTheme::Catppuccin {
name: catppuccin,
accent: catppuccin_value,
} => match catppuccin {
Catppuccin::Frappe => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::FRAPPE);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
auto_select_fill: catppuccin_auto_select_fill,
auto_select_text: catppuccin_auto_select_text,
} => {
match catppuccin {
Catppuccin::Frappe => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::FRAPPE);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::FRAPPE.base);
bg_color.replace(catppuccin_egui::FRAPPE.base);
}
Catppuccin::Latte => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::LATTE);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::LATTE.base);
}
Catppuccin::Macchiato => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MACCHIATO);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MACCHIATO.base);
}
Catppuccin::Mocha => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MOCHA);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MOCHA.base);
}
}
Catppuccin::Latte => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::LATTE);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::LATTE.base);
}
Catppuccin::Macchiato => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MACCHIATO);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MACCHIATO.base);
}
Catppuccin::Mocha => {
catppuccin_egui::set_theme(ctx, catppuccin_egui::MOCHA);
let catppuccin_value = catppuccin_value.unwrap_or_default();
let accent = catppuccin_value.color32(catppuccin.as_theme());
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
style.visuals.override_text_color = None;
});
bg_color.replace(catppuccin_egui::MOCHA.base);
}
},
(
catppuccin_auto_select_fill.map(|c| c.color32(catppuccin.as_theme())),
catppuccin_auto_select_text.map(|c| c.color32(catppuccin.as_theme())),
)
}
KomobarTheme::Base16 {
name: base16,
accent: base16_value,
auto_select_fill: base16_auto_select_fill,
auto_select_text: base16_auto_select_text,
} => {
ctx.set_style(base16.style());
let base16_value = base16_value.unwrap_or_default();
let accent = base16_value.color32(base16);
let accent = base16_value.color32(Base16Wrapper::Base16(base16));
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
@@ -164,8 +180,46 @@ pub fn apply_theme(
});
bg_color.replace(base16.background());
(
base16_auto_select_fill.map(|c| c.color32(Base16Wrapper::Base16(base16))),
base16_auto_select_text.map(|c| c.color32(Base16Wrapper::Base16(base16))),
)
}
}
KomobarTheme::Custom {
colours,
accent: base16_value,
auto_select_fill: base16_auto_select_fill,
auto_select_text: base16_auto_select_text,
} => {
let background = colours.background();
ctx.set_style(colours.style());
let base16_value = base16_value.unwrap_or_default();
let accent = base16_value.color32(Base16Wrapper::Custom(colours.clone()));
ctx.style_mut(|style| {
style.visuals.selection.stroke.color = accent;
style.visuals.widgets.hovered.fg_stroke.color = accent;
style.visuals.widgets.active.fg_stroke.color = accent;
});
bg_color.replace(background);
(
base16_auto_select_fill.map(|c| c.color32(Base16Wrapper::Custom(colours.clone()))),
base16_auto_select_text.map(|c| c.color32(Base16Wrapper::Custom(colours.clone()))),
)
}
};
AUTO_SELECT_FILL_COLOUR.store(
auto_select_fill.map_or(0, |c| Colour::from(c).into()),
Ordering::SeqCst,
);
AUTO_SELECT_TEXT_COLOUR.store(
auto_select_text.map_or(0, |c| Colour::from(c).into()),
Ordering::SeqCst,
);
// Apply transparency_alpha
let theme_color = *bg_color.borrow();
@@ -431,11 +485,11 @@ impl Komobar {
}
fn try_apply_theme(&mut self, ctx: &Context) {
match self.config.theme {
match &self.config.theme {
Some(theme) => {
apply_theme(
ctx,
theme,
theme.clone(),
self.bg_color.clone(),
self.bg_color_with_alpha.clone(),
self.config.transparency_alpha,
@@ -447,13 +501,16 @@ impl Komobar {
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|_| dirs::home_dir().expect("there is no home directory"),
|home_path| {
let home = PathBuf::from(&home_path);
let home = home_path.replace_env();
assert!(
home.is_dir(),
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
home_path
);
home
if home.as_path().is_dir() {
home
} else {
panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
}
},
);
@@ -463,6 +520,26 @@ 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),
@@ -473,17 +550,6 @@ impl Komobar {
self.render_config.clone(),
);
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(name),
};
if let Some(state) = &self.komorebi_notification_state {
state.borrow_mut().stack_accent = Some(stack_accent);
}
@@ -782,7 +848,7 @@ impl eframe::App for Komobar {
self.bg_color_with_alpha.clone(),
self.config.transparency_alpha,
self.config.grouping,
self.config.theme,
self.config.theme.clone(),
self.render_config.clone(),
);
}

View File

@@ -1,19 +1,19 @@
use crate::render::Grouping;
use crate::widget::WidgetConfig;
use crate::widgets::widget::WidgetConfig;
use crate::DEFAULT_PADDING;
use eframe::egui::Pos2;
use eframe::egui::TextBuffer;
use eframe::egui::Vec2;
use komorebi_client::KomorebiTheme;
use komorebi_client::Rect;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
/// The `komorebi.bar.json` configuration file reference for `v0.1.35`
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// The `komorebi.bar.json` configuration file reference for `v0.1.36`
pub struct KomobarConfig {
/// Bar height (default: 50)
pub height: Option<f32>,
@@ -136,7 +136,8 @@ impl KomobarConfig {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct PositionConfig {
/// The desired starting position of the bar (0,0 = top left of the screen)
#[serde(alias = "position")]
@@ -146,13 +147,15 @@ pub struct PositionConfig {
pub end: Option<Position>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct FrameConfig {
/// Margin inside the painted frame
pub inner_margin: Position,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum MonitorConfigOrIndex {
/// The monitor index where you want the bar to show
@@ -161,7 +164,8 @@ pub enum MonitorConfigOrIndex {
MonitorConfig(MonitorConfig),
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct MonitorConfig {
/// Komorebi monitor index of the monitor on which to render the bar
pub index: usize,
@@ -172,7 +176,8 @@ pub struct MonitorConfig {
pub type Padding = SpacingKind;
pub type Margin = SpacingKind;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
// WARNING: To any developer messing with this code in the future: The order here matters!
// `Grouped` needs to come last, otherwise serde might mistaken an `IndividualSpacingConfig` for a
@@ -223,20 +228,23 @@ impl SpacingKind {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct GroupedSpacingConfig {
pub vertical: Option<GroupedSpacingOptions>,
pub horizontal: Option<GroupedSpacingOptions>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum GroupedSpacingOptions {
Symmetrical(f32),
Split(f32, f32),
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct IndividualSpacingConfig {
pub top: f32,
pub bottom: f32,
@@ -338,7 +346,8 @@ impl KomobarConfig {
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Position {
/// X coordinate
pub x: f32,
@@ -364,7 +373,8 @@ impl From<Position> for Pos2 {
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "palette")]
pub enum KomobarTheme {
/// A theme from catppuccin-egui
@@ -372,12 +382,24 @@ pub enum KomobarTheme {
/// Name of the Catppuccin theme (theme previews: https://github.com/catppuccin/catppuccin)
name: komorebi_themes::Catppuccin,
accent: Option<komorebi_themes::CatppuccinValue>,
auto_select_fill: Option<komorebi_themes::CatppuccinValue>,
auto_select_text: Option<komorebi_themes::CatppuccinValue>,
},
/// A theme from base16-egui-themes
Base16 {
/// Name of the Base16 theme (theme previews: https://tinted-theming.github.io/tinted-gallery/)
name: komorebi_themes::Base16,
accent: Option<komorebi_themes::Base16Value>,
auto_select_fill: Option<komorebi_themes::Base16Value>,
auto_select_text: Option<komorebi_themes::Base16Value>,
},
/// A custom Base16 theme
Custom {
/// Colours of the custom Base16 theme palette
colours: Box<komorebi_themes::Base16ColourPalette>,
accent: Option<komorebi_themes::Base16Value>,
auto_select_fill: Option<komorebi_themes::Base16Value>,
auto_select_text: Option<komorebi_themes::Base16Value>,
},
}
@@ -389,18 +411,33 @@ impl From<KomorebiTheme> for KomobarTheme {
} => Self::Catppuccin {
name,
accent: bar_accent,
auto_select_fill: None,
auto_select_text: None,
},
KomorebiTheme::Base16 {
name, bar_accent, ..
} => Self::Base16 {
name,
accent: bar_accent,
auto_select_fill: None,
auto_select_text: None,
},
KomorebiTheme::Custom {
colours,
bar_accent,
..
} => Self::Custom {
colours,
accent: bar_accent,
auto_select_fill: None,
auto_select_text: None,
},
}
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum LabelPrefix {
/// Show no prefix
None,
@@ -412,7 +449,8 @@ pub enum LabelPrefix {
IconAndText,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum DisplayFormat {
/// Show only icon
Icon,
@@ -428,7 +466,8 @@ pub enum DisplayFormat {
macro_rules! extend_enum {
($existing_enum:ident, $new_enum:ident, { $($(#[$meta:meta])* $variant:ident),* $(,)? }) => {
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema, PartialEq)]
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum $new_enum {
// Add new variants
$(
@@ -460,12 +499,12 @@ extend_enum!(DisplayFormat, WorkspacesDisplayFormat, {
#[cfg(test)]
mod tests {
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use serde_json::json;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum OriginalDisplayFormat {
/// Show None Of The Things
NoneOfTheThings,

View File

@@ -1,21 +1,9 @@
mod bar;
mod battery;
mod config;
mod cpu;
mod date;
mod keyboard;
mod komorebi;
mod komorebi_layout;
mod media;
mod memory;
mod network;
mod render;
mod selected_frame;
mod storage;
mod time;
mod ui;
mod update;
mod widget;
mod widgets;
use crate::bar::Komobar;
use crate::config::KomobarConfig;
@@ -28,14 +16,16 @@ use font_loader::system_fonts;
use hotwatch::EventKind;
use hotwatch::Hotwatch;
use image::RgbaImage;
use komorebi_client::replace_env_in_path;
use komorebi_client::PathExt;
use komorebi_client::SocketMessage;
use komorebi_client::SubscribeOptions;
use schemars::gen::SchemaSettings;
use std::collections::HashMap;
use std::io::BufReader;
use std::io::Read;
use std::path::PathBuf;
use std::sync::atomic::AtomicI32;
use std::sync::atomic::AtomicU32;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::LazyLock;
@@ -60,7 +50,10 @@ pub static MONITOR_INDEX: AtomicUsize = AtomicUsize::new(0);
pub static BAR_HEIGHT: f32 = 50.0;
pub static DEFAULT_PADDING: f32 = 10.0;
pub static ICON_CACHE: LazyLock<Mutex<HashMap<String, RgbaImage>>> =
pub static AUTO_SELECT_FILL_COLOUR: AtomicU32 = AtomicU32::new(0);
pub static AUTO_SELECT_TEXT_COLOUR: AtomicU32 = AtomicU32::new(0);
pub static ICON_CACHE: LazyLock<Mutex<HashMap<isize, RgbaImage>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
#[derive(Parser)]
@@ -74,6 +67,7 @@ struct Opts {
fonts: bool,
/// Path to a JSON or YAML configuration file
#[clap(short, long)]
#[clap(value_parser = replace_env_in_path)]
config: Option<PathBuf>,
/// Write an example komorebi.bar.json to disk
#[clap(long)]
@@ -125,8 +119,9 @@ fn main() -> color_eyre::Result<()> {
let opts: Opts = Opts::parse();
#[cfg(feature = "schemars")]
if opts.schema {
let settings = SchemaSettings::default().with(|s| {
let settings = schemars::gen::SchemaSettings::default().with(|s| {
s.option_nullable = false;
s.option_add_null_type = false;
s.inline_subschemas = true;
@@ -167,13 +162,15 @@ fn main() -> color_eyre::Result<()> {
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|_| dirs::home_dir().expect("there is no home directory"),
|home_path| {
let home = PathBuf::from(&home_path);
let home = home_path.replace_env();
if home.as_path().is_dir() {
home
} else {
panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
}
assert!(
home.is_dir(),
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
home_path
);
home
},
);
@@ -182,7 +179,7 @@ fn main() -> color_eyre::Result<()> {
std::fs::write(home_dir.join("komorebi.bar.json"), komorebi_bar_json)?;
println!(
"Example komorebi.bar.json file written to {}",
home_dir.as_path().display()
home_dir.display()
);
std::process::exit(0);
@@ -190,16 +187,11 @@ fn main() -> color_eyre::Result<()> {
let default_config_path = home_dir.join("komorebi.bar.json");
let config_path = opts.config.map_or_else(
|| {
if !default_config_path.is_file() {
None
} else {
Some(default_config_path.clone())
}
},
Option::from,
);
let config_path = opts.config.or_else(|| {
default_config_path
.is_file()
.then_some(default_config_path.clone())
});
let mut config = match config_path {
None => {
@@ -209,17 +201,14 @@ fn main() -> color_eyre::Result<()> {
std::fs::write(&default_config_path, komorebi_bar_json)?;
tracing::info!(
"created example configuration file: {}",
default_config_path.as_path().display()
default_config_path.display()
);
KomobarConfig::read(&default_config_path)?
}
Some(ref config) => {
if !opts.aliases {
tracing::info!(
"found configuration file: {}",
config.as_path().to_string_lossy()
);
tracing::info!("found configuration file: {}", config.display());
}
KomobarConfig::read(config)?
@@ -319,10 +308,7 @@ fn main() -> color_eyre::Result<()> {
hotwatch.watch(config_path, move |event| match event.kind {
EventKind::Modify(_) | EventKind::Remove(_) => match KomobarConfig::read(&config_path_cl) {
Ok(updated) => {
tracing::info!(
"configuration file updated: {}",
config_path_cl.as_path().to_string_lossy()
);
tracing::info!("configuration file updated: {}", config_path_cl.display());
if let Err(error) = tx_config.send(updated) {
tracing::error!("could not send configuration update to gui: {error}")
@@ -349,7 +335,7 @@ fn main() -> color_eyre::Result<()> {
let ctx_komorebi = cc.egui_ctx.clone();
std::thread::spawn(move || {
let subscriber_name = format!("komorebi-bar-{}", random_word::gen(random_word::Lang::En));
let subscriber_name = format!("komorebi-bar-{}", random_word::get(random_word::Lang::En));
let listener = komorebi_client::subscribe_with_options(&subscriber_name, SubscribeOptions {
filter_state_changes: true,

View File

@@ -1,376 +0,0 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use num_derive::FromPrimitive;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::fmt;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
use sysinfo::Networks;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct NetworkConfig {
/// Enable the Network widget
pub enable: bool,
/// Show total data transmitted
pub show_total_data_transmitted: bool,
/// Show network activity
pub show_network_activity: bool,
/// Show default interface
pub show_default_interface: Option<bool>,
/// Characters to reserve for network activity data
pub network_activity_fill_characters: Option<usize>,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<NetworkConfig> for Network {
fn from(value: NetworkConfig) -> Self {
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
Self {
enable: value.enable,
show_total_activity: value.show_total_data_transmitted,
show_activity: value.show_network_activity,
show_default_interface: value.show_default_interface.unwrap_or(true),
networks_network_activity: Networks::new_with_refreshed_list(),
default_interface: String::new(),
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
network_activity_fill_characters: value
.network_activity_fill_characters
.unwrap_or_default(),
last_state_total_activity: vec![],
last_state_activity: vec![],
last_updated_network_activity: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
}
}
}
pub struct Network {
pub enable: bool,
pub show_total_activity: bool,
pub show_activity: bool,
pub show_default_interface: bool,
networks_network_activity: Networks,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
default_interface: String,
last_state_total_activity: Vec<NetworkReading>,
last_state_activity: Vec<NetworkReading>,
last_updated_network_activity: Instant,
network_activity_fill_characters: usize,
}
impl Network {
fn default_interface(&mut self) {
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
self.default_interface.clone_from(friendly_name);
}
}
}
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
let mut activity = self.last_state_activity.clone();
let mut total_activity = self.last_state_total_activity.clone();
let now = Instant::now();
if now.duration_since(self.last_updated_network_activity)
> Duration::from_secs(self.data_refresh_interval)
{
activity.clear();
total_activity.clear();
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
self.default_interface.clone_from(friendly_name);
self.networks_network_activity.refresh(true);
for (interface_name, data) in &self.networks_network_activity {
if friendly_name.eq(interface_name) {
if self.show_activity {
activity.push(NetworkReading::new(
NetworkReadingFormat::Speed,
Self::to_pretty_bytes(
data.received(),
self.data_refresh_interval,
),
Self::to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval,
),
));
}
if self.show_total_activity {
total_activity.push(NetworkReading::new(
NetworkReadingFormat::Total,
Self::to_pretty_bytes(data.total_received(), 1),
Self::to_pretty_bytes(data.total_transmitted(), 1),
))
}
}
}
}
}
self.last_state_activity.clone_from(&activity);
self.last_state_total_activity.clone_from(&total_activity);
self.last_updated_network_activity = now;
}
(activity, total_activity)
}
fn reading_to_label(
&self,
ctx: &Context,
reading: NetworkReading,
config: RenderConfig,
) -> Label {
let (text_down, text_up) = match self.label_prefix {
LabelPrefix::None | LabelPrefix::Icon => match reading.format {
NetworkReadingFormat::Speed => (
format!(
"{: >width$}/s ",
reading.received_text,
width = self.network_activity_fill_characters
),
format!(
"{: >width$}/s",
reading.transmitted_text,
width = self.network_activity_fill_characters
),
),
NetworkReadingFormat::Total => (
format!("{} ", reading.received_text),
reading.transmitted_text,
),
},
LabelPrefix::Text | LabelPrefix::IconAndText => match reading.format {
NetworkReadingFormat::Speed => (
format!(
"DOWN: {: >width$}/s ",
reading.received_text,
width = self.network_activity_fill_characters
),
format!(
"UP: {: >width$}/s",
reading.transmitted_text,
width = self.network_activity_fill_characters
),
),
NetworkReadingFormat::Total => (
format!("\u{2211}DOWN: {}/s ", reading.received_text),
format!("\u{2211}UP: {}/s", reading.transmitted_text),
),
},
};
let icon_format = TextFormat::simple(
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
);
let text_format = TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
};
// icon
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::ARROW_FAT_DOWN.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
icon_format.font_id.clone(),
icon_format.color,
100.0,
);
// text
layout_job.append(
&text_down,
ctx.style().spacing.item_spacing.x,
text_format.clone(),
);
// icon
layout_job.append(
&match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::ARROW_FAT_UP.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
0.0,
icon_format.clone(),
);
// text
layout_job.append(
&text_up,
ctx.style().spacing.item_spacing.x,
text_format.clone(),
);
Label::new(layout_job).selectable(false)
}
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> String {
let input = input_in_bytes as f32 / timespan_in_s as f32;
let mut magnitude = input.log(1024f32) as u32;
// let the base unit be KiB
if magnitude < 1 {
magnitude = 1;
}
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
let result = input / ((1u64) << (magnitude * 10)) as f32;
match base {
Some(DataUnit::B) => format!("{result:.1} B"),
Some(unit) => format!("{result:.1} {unit}iB"),
None => String::from("Unknown data unit"),
}
}
}
impl BarWidget for Network {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
// widget spacing: make sure to use the same config to call the apply_on_widget function
let mut render_config = config.clone();
if self.show_total_activity || self.show_activity {
let (activity, total_activity) = self.network_activity();
if self.show_total_activity {
for reading in total_activity {
render_config.apply_on_widget(true, ui, |ui| {
ui.add(self.reading_to_label(ctx, reading, config.clone()));
});
}
}
if self.show_activity {
for reading in activity {
render_config.apply_on_widget(true, ui, |ui| {
ui.add(self.reading_to_label(ctx, reading, config.clone()));
});
}
}
}
if self.show_default_interface {
self.default_interface();
if !self.default_interface.is_empty() {
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::WIFI_HIGH.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
self.default_interface.insert_str(0, "NET: ");
}
layout_job.append(
&self.default_interface,
10.0,
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
},
);
render_config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn()
{
eprintln!("{}", error)
}
}
});
}
}
// widget spacing: pass on the config that was use for calling the apply_on_widget function
*config = render_config.clone();
}
}
}
#[derive(Clone)]
enum NetworkReadingFormat {
Speed = 0,
Total = 1,
}
#[derive(Clone)]
struct NetworkReading {
pub format: NetworkReadingFormat,
pub received_text: String,
pub transmitted_text: String,
}
impl NetworkReading {
pub fn new(format: NetworkReadingFormat, received: String, transmitted: String) -> Self {
NetworkReading {
format,
received_text: received,
transmitted_text: transmitted,
}
}
}
#[derive(Debug, FromPrimitive)]
enum DataUnit {
B = 0,
K = 1,
M = 2,
G = 3,
T = 4,
P = 5,
E = 6,
Z = 7,
Y = 8,
}
impl fmt::Display for DataUnit {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}

View File

@@ -1,6 +1,8 @@
use crate::bar::Alignment;
use crate::config::KomobarConfig;
use crate::config::MonitorConfigOrIndex;
use crate::AUTO_SELECT_FILL_COLOUR;
use crate::AUTO_SELECT_TEXT_COLOUR;
use eframe::egui::Color32;
use eframe::egui::Context;
use eframe::egui::CornerRadius;
@@ -11,16 +13,19 @@ use eframe::egui::Margin;
use eframe::egui::Shadow;
use eframe::egui::TextStyle;
use eframe::egui::Ui;
use schemars::JsonSchema;
use komorebi_client::Colour;
use komorebi_client::Rgb;
use serde::Deserialize;
use serde::Serialize;
use std::num::NonZeroU32;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::Arc;
static SHOW_KOMOREBI_LAYOUT_OPTIONS: AtomicUsize = AtomicUsize::new(0);
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "kind")]
pub enum Grouping {
/// No grouping is applied
@@ -55,6 +60,10 @@ pub struct RenderConfig {
pub icon_font_id: FontId,
/// Show all icons on the workspace section of the Komorebi widget
pub show_all_icons: bool,
/// Background color of the selected frame
pub auto_select_fill: Option<Color32>,
/// Text color of the selected frame
pub auto_select_text: Option<Color32>,
}
pub trait RenderExt {
@@ -108,6 +117,10 @@ impl RenderExt for &KomobarConfig {
text_font_id,
icon_font_id,
show_all_icons,
auto_select_fill: NonZeroU32::new(AUTO_SELECT_FILL_COLOUR.load(Ordering::SeqCst))
.map(|c| Colour::Rgb(Rgb::from(c.get())).into()),
auto_select_text: NonZeroU32::new(AUTO_SELECT_TEXT_COLOUR.load(Ordering::SeqCst))
.map(|c| Colour::Rgb(Rgb::from(c.get())).into()),
}
}
}
@@ -133,6 +146,8 @@ impl RenderConfig {
text_font_id: FontId::default(),
icon_font_id: FontId::default(),
show_all_icons: false,
auto_select_fill: None,
auto_select_text: None,
}
}
@@ -339,7 +354,8 @@ impl RenderConfig {
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct GroupingConfig {
/// Styles for the grouping
pub style: Option<GroupingStyle>,
@@ -349,7 +365,8 @@ pub struct GroupingConfig {
pub rounding: Option<RoundingConfig>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum GroupingStyle {
#[serde(alias = "CtByte")]
Default,
@@ -369,7 +386,8 @@ pub enum GroupingStyle {
DefaultWithGlowB0O1S2,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum RoundingConfig {
/// All 4 corners are the same

View File

@@ -10,15 +10,29 @@ use eframe::egui::Ui;
/// Same as SelectableLabel, but supports all content
pub struct SelectableFrame {
selected: bool,
selected_fill: Option<Color32>,
}
impl SelectableFrame {
pub fn new(selected: bool) -> Self {
Self { selected }
Self {
selected,
selected_fill: None,
}
}
pub fn new_auto(selected: bool, selected_fill: Option<Color32>) -> Self {
Self {
selected,
selected_fill,
}
}
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> Response {
let Self { selected } = self;
let Self {
selected,
selected_fill,
} = self;
Frame::NONE
.show(ui, |ui| {
@@ -32,7 +46,16 @@ impl SelectableFrame {
);
// since the stroke is drawn inside the frame, we always reserve space for it
if response.hovered() || response.highlighted() || response.has_focus() {
if selected && response.hovered() {
let visuals = ui.style().interact_selectable(&response, selected);
Frame::NONE
.stroke(Stroke::new(1.0, visuals.bg_stroke.color))
.corner_radius(visuals.corner_radius)
.fill(selected_fill.unwrap_or(visuals.bg_fill))
.inner_margin(inner_margin)
.show(ui, add_contents);
} else if response.hovered() || response.highlighted() || response.has_focus() {
let visuals = ui.style().interact_selectable(&response, selected);
Frame::NONE
@@ -47,7 +70,7 @@ impl SelectableFrame {
Frame::NONE
.stroke(Stroke::new(1.0, visuals.bg_fill))
.corner_radius(visuals.corner_radius)
.fill(visuals.bg_fill)
.fill(selected_fill.unwrap_or(visuals.bg_fill))
.inner_margin(inner_margin)
.show(ui, add_contents);
} else {

View File

@@ -1,14 +1,13 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use crate::widgets::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use starship_battery::units::ratio::percent;
@@ -18,7 +17,8 @@ use std::process::Command;
use std::time::Duration;
use std::time::Instant;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct BatteryConfig {
/// Enable the Battery widget
pub enable: bool,
@@ -28,6 +28,8 @@ pub struct BatteryConfig {
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
/// Select when the current percentage is under this value [[1-100]]
pub auto_select_under: Option<u8>,
}
impl From<BatteryConfig> for Battery {
@@ -38,9 +40,10 @@ impl From<BatteryConfig> for Battery {
enable: value.enable,
hide_on_full_charge: value.hide_on_full_charge.unwrap_or(false),
manager: Manager::new().unwrap(),
last_state: String::new(),
last_state: None,
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
auto_select_under: value.auto_select_under.map(|u| u.clamp(1, 100)),
state: BatteryState::Discharging,
last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
@@ -52,6 +55,16 @@ impl From<BatteryConfig> for Battery {
pub enum BatteryState {
Charging,
Discharging,
High,
Medium,
Low,
Warning,
}
#[derive(Clone, Debug)]
struct BatteryOutput {
label: String,
selected: bool,
}
pub struct Battery {
@@ -61,37 +74,53 @@ pub struct Battery {
pub state: BatteryState,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
last_state: String,
auto_select_under: Option<u8>,
last_state: Option<BatteryOutput>,
last_updated: Instant,
}
impl Battery {
fn output(&mut self) -> String {
fn output(&mut self) -> Option<BatteryOutput> {
let mut output = self.last_state.clone();
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
output.clear();
output = None;
if let Ok(mut batteries) = self.manager.batteries() {
if let Some(Ok(first)) = batteries.nth(0) {
let percentage = first.state_of_charge().get::<percent>();
let percentage = first.state_of_charge().get::<percent>().round() as u8;
if percentage == 100.0 && self.hide_on_full_charge {
output = String::new()
if percentage == 100 && self.hide_on_full_charge {
output = None
} else {
match first.state() {
State::Charging => self.state = BatteryState::Charging,
State::Discharging => self.state = BatteryState::Discharging,
State::Discharging => {
self.state = match percentage {
p if p > 75 => BatteryState::Discharging,
p if p > 50 => BatteryState::High,
p if p > 25 => BatteryState::Medium,
p if p > 10 => BatteryState::Low,
_ => BatteryState::Warning,
}
}
_ => {}
}
output = match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage:.0}%")
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage:.0}%"),
}
let selected = self.auto_select_under.is_some_and(|u| percentage <= u);
output = Some(BatteryOutput {
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("BAT: {percentage}%")
}
LabelPrefix::None | LabelPrefix::Icon => {
format!("{percentage}%")
}
},
selected,
})
}
}
}
@@ -108,35 +137,43 @@ impl BarWidget for Battery {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let output = self.output();
if !output.is_empty() {
if let Some(output) = output {
let emoji = match self.state {
BatteryState::Charging => egui_phosphor::regular::BATTERY_CHARGING,
BatteryState::Discharging => egui_phosphor::regular::BATTERY_FULL,
BatteryState::High => egui_phosphor::regular::BATTERY_HIGH,
BatteryState::Medium => egui_phosphor::regular::BATTERY_MEDIUM,
BatteryState::Low => egui_phosphor::regular::BATTERY_LOW,
BatteryState::Warning => egui_phosphor::regular::BATTERY_WARNING,
};
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => emoji.to_string(),
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0,
);
layout_job.append(
&output,
&output.label,
10.0,
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
valign: Align::Center,
..Default::default()
},
);
let auto_focus_fill = config.auto_select_fill;
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{

View File

@@ -1,14 +1,13 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use crate::widgets::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
@@ -17,7 +16,8 @@ use std::time::Instant;
use sysinfo::RefreshKind;
use sysinfo::System;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CpuConfig {
/// Enable the Cpu widget
pub enable: bool,
@@ -25,6 +25,8 @@ pub struct CpuConfig {
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
/// Select when the current percentage is over this value [[1-100]]
pub auto_select_over: Option<u8>,
}
impl From<CpuConfig> for Cpu {
@@ -38,6 +40,7 @@ impl From<CpuConfig> for Cpu {
),
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
@@ -45,26 +48,38 @@ impl From<CpuConfig> for Cpu {
}
}
#[derive(Clone, Debug)]
struct CpuOutput {
label: String,
selected: bool,
}
pub struct Cpu {
pub enable: bool,
system: System,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
auto_select_over: Option<u8>,
last_updated: Instant,
}
impl Cpu {
fn output(&mut self) -> String {
fn output(&mut self) -> CpuOutput {
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.system.refresh_cpu_usage();
self.last_updated = now;
}
let used = self.system.global_cpu_usage();
match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {:.0}%", used),
LabelPrefix::None | LabelPrefix::Icon => format!("{:.0}%", used),
let used = self.system.global_cpu_usage() as u8;
let selected = self.auto_select_over.is_some_and(|o| used >= o);
CpuOutput {
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => format!("CPU: {}%", used),
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", used),
},
selected,
}
}
}
@@ -73,7 +88,9 @@ impl BarWidget for Cpu {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let output = self.output();
if !output.is_empty() {
if !output.label.is_empty() {
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
@@ -82,23 +99,25 @@ impl BarWidget for Cpu {
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0,
);
layout_job.append(
&output,
&output.label,
10.0,
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
valign: Align::Center,
..Default::default()
},
);
let auto_focus_fill = config.auto_select_fill;
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{

View File

@@ -1,7 +1,9 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use crate::widgets::widget::BarWidget;
use chrono::Local;
use chrono_tz::Tz;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
@@ -9,12 +11,14 @@ use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::WidgetText;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::time::Duration;
use std::time::Instant;
/// Custom format with additive modifiers for integer format specifiers
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CustomModifiers {
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
format: String,
@@ -38,7 +42,7 @@ impl CustomModifiers {
}
// get the strftime value of modifier
let formatted_modifier = chrono::Local::now().format(modifier).to_string();
let formatted_modifier = Local::now().format(modifier).to_string();
// find the gotten value in the original output
if let Some(pos) = modified_output.find(&formatted_modifier) {
@@ -55,7 +59,8 @@ impl CustomModifiers {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct DateConfig {
/// Enable the Date widget
pub enable: bool,
@@ -63,19 +68,41 @@ pub struct DateConfig {
pub format: DateFormat,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
/// TimeZone (https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html)
///
/// Use a custom format to display additional information, i.e.:
/// ```json
/// {
/// "Date": {
/// "enable": true,
/// "format": { "Custom": "%D %Z (Tokyo)" },
/// "timezone": "Asia/Tokyo"
/// }
///}
/// ```
pub timezone: Option<String>,
}
impl From<DateConfig> for Date {
fn from(value: DateConfig) -> Self {
let data_refresh_interval = 1;
Self {
enable: value.enable,
format: value.format,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
timezone: value.timezone,
data_refresh_interval,
last_state: String::new(),
last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum DateFormat {
/// Month/Date/Year format (09/08/24)
MonthDateYear,
@@ -119,19 +146,46 @@ pub struct Date {
pub enable: bool,
pub format: DateFormat,
label_prefix: LabelPrefix,
timezone: Option<String>,
data_refresh_interval: u64,
last_state: String,
last_updated: Instant,
}
impl Date {
fn output(&mut self) -> String {
let formatted = chrono::Local::now()
.format(&self.format.fmt_string())
.to_string();
let mut output = self.last_state.clone();
let now = Instant::now();
// if custom modifiers are used, apply them
match &self.format {
DateFormat::CustomModifiers(custom) => custom.apply(&formatted),
_ => formatted,
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
let formatted = match &self.timezone {
Some(timezone) => match timezone.parse::<Tz>() {
Ok(tz) => Local::now()
.with_timezone(&tz)
.format(&self.format.fmt_string())
.to_string()
.trim()
.to_string(),
Err(_) => format!("Invalid timezone: {}", timezone),
},
None => Local::now()
.format(&self.format.fmt_string())
.to_string()
.trim()
.to_string(),
};
// if custom modifiers are used, apply them
output = match &self.format {
DateFormat::CustomModifiers(custom) => custom.apply(&formatted),
_ => formatted,
};
self.last_state.clone_from(&output);
self.last_updated = now;
}
output
}
}

View File

@@ -1,6 +1,6 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::widget::BarWidget;
use crate::widgets::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
@@ -8,7 +8,6 @@ use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::WidgetText;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::time::Duration;
@@ -23,7 +22,8 @@ use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
const DEFAULT_DATA_REFRESH_INTERVAL: u64 = 1;
const ERROR_TEXT: &str = "Error";
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct KeyboardConfig {
/// Enable the Input widget
pub enable: bool,

View File

@@ -2,16 +2,18 @@ use crate::bar::apply_theme;
use crate::config::DisplayFormat;
use crate::config::KomobarTheme;
use crate::config::WorkspacesDisplayFormat;
use crate::komorebi_layout::KomorebiLayout;
use crate::render::Grouping;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::ui::CustomUi;
use crate::widget::BarWidget;
use crate::widgets::komorebi_layout::KomorebiLayout;
use crate::widgets::widget::BarWidget;
use crate::ICON_CACHE;
use crate::MAX_LABEL_WIDTH;
use crate::MONITOR_INDEX;
use eframe::egui::text::LayoutJob;
use eframe::egui::vec2;
use eframe::egui::Align;
use eframe::egui::Color32;
use eframe::egui::ColorImage;
use eframe::egui::Context;
@@ -24,6 +26,7 @@ use eframe::egui::RichText;
use eframe::egui::Sense;
use eframe::egui::Stroke;
use eframe::egui::StrokeKind;
use eframe::egui::TextFormat;
use eframe::egui::TextureHandle;
use eframe::egui::TextureOptions;
use eframe::egui::Ui;
@@ -37,7 +40,6 @@ use komorebi_client::SocketMessage;
use komorebi_client::Window;
use komorebi_client::Workspace;
use komorebi_client::WorkspaceLayer;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::cell::RefCell;
@@ -47,7 +49,8 @@ use std::path::PathBuf;
use std::rc::Rc;
use std::sync::atomic::Ordering;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct KomorebiConfig {
/// Configure the Workspaces widget
pub workspaces: Option<KomorebiWorkspacesConfig>,
@@ -55,13 +58,17 @@ pub struct KomorebiConfig {
pub layout: Option<KomorebiLayoutConfig>,
/// Configure the Workspace Layer widget
pub workspace_layer: Option<KomorebiWorkspaceLayerConfig>,
/// Configure the Focused Window widget
pub focused_window: Option<KomorebiFocusedWindowConfig>,
/// Configure the Focused Container widget
#[serde(alias = "focused_window")]
pub focused_container: Option<KomorebiFocusedContainerConfig>,
/// Configure the Locked Container widget
pub locked_container: Option<KomorebiLockedContainerConfig>,
/// Configure the Configuration Switcher widget
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct KomorebiWorkspacesConfig {
/// Enable the Komorebi Workspaces widget
pub enable: bool,
@@ -71,7 +78,8 @@ pub struct KomorebiWorkspacesConfig {
pub display: Option<WorkspacesDisplayFormat>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct KomorebiLayoutConfig {
/// Enable the Komorebi Layout widget
pub enable: bool,
@@ -81,7 +89,8 @@ pub struct KomorebiLayoutConfig {
pub display: Option<DisplayFormat>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct KomorebiWorkspaceLayerConfig {
/// Enable the Komorebi Workspace Layer widget
pub enable: bool,
@@ -91,17 +100,30 @@ pub struct KomorebiWorkspaceLayerConfig {
pub show_when_tiling: Option<bool>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiFocusedWindowConfig {
/// Enable the Komorebi Focused Window widget
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct KomorebiFocusedContainerConfig {
/// Enable the Komorebi Focused Container widget
pub enable: bool,
/// DEPRECATED: use 'display' instead (Show the icon of the currently focused window)
/// DEPRECATED: use 'display' instead (Show the icon of the currently focused container)
pub show_icon: Option<bool>,
/// Display format of the currently focused window
/// Display format of the currently focused container
pub display: Option<DisplayFormat>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct KomorebiLockedContainerConfig {
/// Enable the Komorebi Locked Container widget
pub enable: bool,
/// Display format of the current locked state
pub display: Option<DisplayFormat>,
/// Show the widget event if the layer is unlocked
pub show_when_unlocked: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct KomorebiConfigurationSwitcherConfig {
/// Enable the Komorebi Configurations widget
pub enable: bool,
@@ -135,15 +157,19 @@ impl From<&KomorebiConfig> for Komorebi {
.unwrap_or_default(),
mouse_follows_focus: true,
work_area_offset: None,
focused_container_information: KomorebiNotificationStateContainerInformation::EMPTY,
focused_container_information: (
false,
KomorebiNotificationStateContainerInformation::EMPTY,
),
stack_accent: None,
monitor_index: MONITOR_INDEX.load(Ordering::SeqCst),
monitor_usr_idx_map: HashMap::new(),
})),
workspaces: value.workspaces,
layout: value.layout.clone(),
focused_window: value.focused_window,
focused_container: value.focused_container,
workspace_layer: value.workspace_layer,
locked_container: value.locked_container,
configuration_switcher,
}
}
@@ -154,8 +180,9 @@ pub struct Komorebi {
pub komorebi_notification_state: Rc<RefCell<KomorebiNotificationState>>,
pub workspaces: Option<KomorebiWorkspacesConfig>,
pub layout: Option<KomorebiLayoutConfig>,
pub focused_window: Option<KomorebiFocusedWindowConfig>,
pub focused_container: Option<KomorebiFocusedContainerConfig>,
pub workspace_layer: Option<KomorebiWorkspaceLayerConfig>,
pub locked_container: Option<KomorebiLockedContainerConfig>,
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
}
@@ -173,9 +200,10 @@ impl BarWidget for Komorebi {
let format = workspaces.display.unwrap_or(DisplayFormat::Text.into());
config.apply_on_widget(false, ui, |ui| {
for (i, (ws, containers, _)) in
for (i, (ws, containers, _, should_show)) in
komorebi_notification_state.workspaces.iter().enumerate()
{
if *should_show {
let is_selected = komorebi_notification_state.selected_workspace.eq(ws);
if SelectableFrame::new(
@@ -264,7 +292,6 @@ impl BarWidget for Komorebi {
komorebi_notification_state.monitor_index,
i,
),
SocketMessage::RetileWithResizeDimensions,
SocketMessage::MouseFollowsFocus(true),
])
.is_err()
@@ -273,7 +300,6 @@ impl BarWidget for Komorebi {
"could not send the following batch of messages to komorebi:\n
MouseFollowsFocus(false)\n
FocusMonitorWorkspaceNumber({}, {})\n
RetileWithResizeDimensions
MouseFollowsFocus(true)\n",
komorebi_notification_state.monitor_index,
i,
@@ -284,19 +310,18 @@ impl BarWidget for Komorebi {
komorebi_notification_state.monitor_index,
i,
),
SocketMessage::RetileWithResizeDimensions,
])
.is_err()
{
tracing::error!(
"could not send the following batch of messages to komorebi:\n
FocusMonitorWorkspaceNumber({}, {})\n
RetileWithResizeDimensions",
FocusMonitorWorkspaceNumber({}, {})\n",
komorebi_notification_state.monitor_index,
i,
);
}
}
}
}
});
}
@@ -313,7 +338,7 @@ impl BarWidget for Komorebi {
.workspaces
.iter()
.find(|o| komorebi_notification_state.selected_workspace.eq(&o.0))
.map(|(_, _, layer)| layer);
.map(|(_, _, layer, _)| layer);
if let Some(layer) = layer {
if (layer_config.show_when_tiling.unwrap_or_default()
@@ -330,7 +355,7 @@ impl BarWidget for Komorebi {
if matches!(layer, WorkspaceLayer::Tiling) {
let (response, painter) =
ui.allocate_painter(size, Sense::hover());
let color = ui.style().visuals.text_color();
let color = ctx.style().visuals.selection.stroke.color;
let stroke = Stroke::new(1.0, color);
let mut rect = response.rect;
let corner =
@@ -360,7 +385,7 @@ impl BarWidget for Komorebi {
} else {
let (response, painter) =
ui.allocate_painter(size, Sense::hover());
let color = ui.style().visuals.text_color();
let color = ctx.style().visuals.selection.stroke.color;
let stroke = Stroke::new(1.0, color);
let mut rect = response.rect;
let corner =
@@ -398,6 +423,7 @@ impl BarWidget for Komorebi {
if layer_frame.clicked()
&& komorebi_client::send_batch([
SocketMessage::FocusMonitorAtCursor,
SocketMessage::MouseFollowsFocus(false),
SocketMessage::ToggleWorkspaceLayer,
SocketMessage::MouseFollowsFocus(
@@ -442,72 +468,109 @@ impl BarWidget for Komorebi {
for (name, location) in configuration_switcher.configurations.iter() {
let path = PathBuf::from(location);
if path.is_file() {
config.apply_on_widget(false, ui,|ui|{
if SelectableFrame::new(false).show(ui, |ui|{
ui.add(Label::new(name).selectable(false))
})
.clicked()
{
let canonicalized = dunce::canonicalize(path.clone()).unwrap_or(path);
let mut proceed = true;
if komorebi_client::send_message(&SocketMessage::ReplaceConfiguration(
canonicalized,
))
.is_err()
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(name).selectable(false)))
.clicked()
{
tracing::error!(
"could not send message to komorebi: ReplaceConfiguration"
);
proceed = false;
}
let canonicalized =
dunce::canonicalize(path.clone()).unwrap_or(path);
if let Some(rect) = komorebi_notification_state.work_area_offset {
if proceed {
match komorebi_client::send_query(&SocketMessage::Query(
komorebi_client::StateQuery::FocusedMonitorIndex,
)) {
Ok(idx) => {
if let Ok(monitor_idx) = idx.parse::<usize>() {
if komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
monitor_idx,
rect,
),
)
.is_err()
{
tracing::error!(
"could not send message to komorebi: MonitorWorkAreaOffset"
);
}
}
}
Err(_) => {
tracing::error!(
"could not send message to komorebi: Query"
);
}
}
if komorebi_client::send_message(
&SocketMessage::ReplaceConfiguration(canonicalized),
)
.is_err()
{
tracing::error!(
"could not send message to komorebi: ReplaceConfiguration"
);
}
}
}});
});
}
}
}
}
if let Some(focused_window) = self.focused_window {
if focused_window.enable {
if let Some(locked_container_config) = self.locked_container {
if locked_container_config.enable {
let is_locked = komorebi_notification_state.focused_container_information.0;
if locked_container_config
.show_when_unlocked
.unwrap_or_default()
|| is_locked
{
let titles = &komorebi_notification_state
.focused_container_information
.1
.titles;
if !titles.is_empty() {
let display_format = locked_container_config
.display
.unwrap_or(DisplayFormat::Text);
let mut layout_job = LayoutJob::simple(
if display_format != DisplayFormat::Text {
if is_locked {
egui_phosphor::regular::LOCK_KEY.to_string()
} else {
egui_phosphor::regular::LOCK_SIMPLE_OPEN.to_string()
}
} else {
String::new()
},
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
if display_format != DisplayFormat::Icon {
layout_job.append(
if is_locked { "Locked" } else { "Unlocked" },
10.0,
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
},
);
}
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
&& komorebi_client::send_batch([
SocketMessage::FocusMonitorAtCursor,
SocketMessage::ToggleLock,
])
.is_err()
{
tracing::error!("could not send ToggleLock");
}
});
}
}
}
}
if let Some(focused_container_config) = self.focused_container {
if focused_container_config.enable {
let titles = &komorebi_notification_state
.focused_container_information
.1
.titles;
if !titles.is_empty() {
config.apply_on_widget(false, ui, |ui| {
let icons = &komorebi_notification_state
.focused_container_information
.focused_container_information.1
.icons;
let focused_window_idx = komorebi_notification_state
.focused_container_information
.focused_container_information.1
.focused_window_idx;
let iter = titles.iter().zip(icons.iter());
@@ -515,13 +578,13 @@ impl BarWidget for Komorebi {
for (i, (title, icon)) in iter.enumerate() {
let selected = i == focused_window_idx && len != 1;
let text_color = if selected { ctx.style().visuals.selection.stroke.color} else { ui.style().visuals.text_color() };
let text_color = if selected { ctx.style().visuals.selection.stroke.color } else { ui.style().visuals.text_color() };
if SelectableFrame::new(selected)
.show(ui, |ui| {
// handle legacy setting
let format = focused_window.display.unwrap_or(
if focused_window.show_icon.unwrap_or(false) {
let format = focused_container_config.display.unwrap_or(
if focused_container_config.show_icon.unwrap_or(false) {
DisplayFormat::IconAndText
} else {
DisplayFormat::Text
@@ -621,9 +684,10 @@ pub struct KomorebiNotificationState {
String,
Vec<(bool, KomorebiNotificationStateContainerInformation)>,
WorkspaceLayer,
bool,
)>,
pub selected_workspace: String,
pub focused_container_information: KomorebiNotificationStateContainerInformation,
pub focused_container_information: (bool, KomorebiNotificationStateContainerInformation),
pub layout: KomorebiLayout,
pub hide_empty_workspaces: bool,
pub mouse_follows_focus: bool,
@@ -689,7 +753,7 @@ impl KomorebiNotificationState {
SocketMessage::Theme(theme) => {
apply_theme(
ctx,
KomobarTheme::from(theme),
KomobarTheme::from(*theme),
bg_color,
bg_color_with_alpha.clone(),
transparency_alpha,
@@ -736,42 +800,41 @@ impl KomorebiNotificationState {
true
};
if should_show {
workspaces.push((
ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)),
if show_all_icons {
let mut containers = vec![];
let mut has_monocle = false;
workspaces.push((
ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)),
if show_all_icons {
let mut containers = vec![];
let mut has_monocle = false;
// add monocle container
if let Some(container) = ws.monocle_container() {
containers.push((true, container.into()));
has_monocle = true;
}
// add monocle container
if let Some(container) = ws.monocle_container() {
containers.push((true, container.into()));
has_monocle = true;
}
// add all tiled windows
for (i, container) in ws.containers().iter().enumerate() {
containers.push((
!has_monocle && i == ws.focused_container_idx(),
container.into(),
));
}
// add all tiled windows
for (i, container) in ws.containers().iter().enumerate() {
containers.push((
!has_monocle && i == ws.focused_container_idx(),
container.into(),
));
}
// add all floating windows
for floating_window in ws.floating_windows() {
containers.push((
!has_monocle && floating_window.is_focused(),
floating_window.into(),
));
}
// add all floating windows
for floating_window in ws.floating_windows() {
containers.push((
!has_monocle && floating_window.is_focused(),
floating_window.into(),
));
}
containers
} else {
vec![(true, ws.into())]
},
ws.layer().to_owned(),
));
}
containers
} else {
vec![(true, ws.into())]
},
ws.layer().to_owned(),
should_show,
));
}
self.workspaces = workspaces;
@@ -792,7 +855,12 @@ impl KomorebiNotificationState {
};
}
self.focused_container_information = (&monitor.workspaces()[focused_workspace_idx]).into();
let focused_workspace = &monitor.workspaces()[focused_workspace_idx];
let is_focused = focused_workspace
.locked_containers()
.contains(&focused_workspace.focused_container_idx());
self.focused_container_information = (is_focused, focused_workspace.into());
}
}
@@ -831,11 +899,16 @@ impl From<&Container> for KomorebiNotificationStateContainerInformation {
for window in windows {
let mut icon_cache = ICON_CACHE.lock().unwrap();
let mut update_cache = false;
let exe = window.exe().unwrap_or_default();
let hwnd = window.hwnd;
match icon_cache.get(&exe) {
match icon_cache.get(&hwnd) {
None => {
icons.push(windows_icons::get_icon_by_process_id(window.process_id()));
let icon = match windows_icons::get_icon_by_hwnd(window.hwnd) {
None => windows_icons_fallback::get_icon_by_process_id(window.process_id()),
Some(icon) => Some(icon),
};
icons.push(icon);
update_cache = true;
}
Some(icon) => {
@@ -845,7 +918,7 @@ impl From<&Container> for KomorebiNotificationStateContainerInformation {
if update_cache {
if let Some(Some(icon)) = icons.last() {
icon_cache.insert(exe, icon.clone());
icon_cache.insert(hwnd, icon.clone());
}
}
}
@@ -867,11 +940,16 @@ impl From<&Window> for KomorebiNotificationStateContainerInformation {
let mut icon_cache = ICON_CACHE.lock().unwrap();
let mut update_cache = false;
let mut icons = vec![];
let exe = value.exe().unwrap_or_default();
let hwnd = value.hwnd;
match icon_cache.get(&exe) {
match icon_cache.get(&hwnd) {
None => {
icons.push(windows_icons::get_icon_by_process_id(value.process_id()));
let icon = match windows_icons::get_icon_by_hwnd(hwnd) {
None => windows_icons_fallback::get_icon_by_process_id(value.process_id()),
Some(icon) => Some(icon),
};
icons.push(icon);
update_cache = true;
}
Some(icon) => {
@@ -881,7 +959,7 @@ impl From<&Window> for KomorebiNotificationStateContainerInformation {
if update_cache {
if let Some(Some(icon)) = icons.last() {
icon_cache.insert(exe, icon.clone());
icon_cache.insert(hwnd, icon.clone());
}
}

View File

@@ -1,7 +1,7 @@
use crate::config::DisplayFormat;
use crate::komorebi::KomorebiLayoutConfig;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widgets::komorebi::KomorebiLayoutConfig;
use eframe::egui::vec2;
use eframe::egui::Context;
use eframe::egui::CornerRadius;
@@ -14,7 +14,6 @@ use eframe::egui::StrokeKind;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use komorebi_client::SocketMessage;
use schemars::JsonSchema;
use serde::de::Error;
use serde::Deserialize;
use serde::Deserializer;
@@ -23,7 +22,8 @@ use serde_json::from_str;
use std::fmt::Display;
use std::fmt::Formatter;
#[derive(Copy, Clone, Debug, Serialize, JsonSchema, PartialEq)]
#[derive(Copy, Clone, Debug, Serialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum KomorebiLayout {
Default(komorebi_client::DefaultLayout),
@@ -105,12 +105,22 @@ impl KomorebiLayout {
}
}
KomorebiLayout::Monocle => {
if komorebi_client::send_message(&SocketMessage::ToggleMonocle).is_err() {
if komorebi_client::send_batch([
SocketMessage::FocusMonitorAtCursor,
SocketMessage::ToggleMonocle,
])
.is_err()
{
tracing::error!("could not send message to komorebi: ToggleMonocle");
}
}
KomorebiLayout::Floating => {
if komorebi_client::send_message(&SocketMessage::ToggleTiling).is_err() {
if komorebi_client::send_batch([
SocketMessage::FocusMonitorAtCursor,
SocketMessage::ToggleTiling,
])
.is_err()
{
tracing::error!("could not send message to komorebi: ToggleTiling");
}
}
@@ -241,7 +251,7 @@ impl KomorebiLayout {
let layout_frame = SelectableFrame::new(false)
.show(ui, |ui| {
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format {
self.show_icon(false, font_id.clone(), ctx, ui);
self.show_icon(true, font_id.clone(), ctx, ui);
}
if let DisplayFormat::Text | DisplayFormat::IconAndText = format {

View File

@@ -1,7 +1,7 @@
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::ui::CustomUi;
use crate::widget::BarWidget;
use crate::widgets::widget::BarWidget;
use crate::MAX_LABEL_WIDTH;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
@@ -10,13 +10,13 @@ use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::sync::atomic::Ordering;
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct MediaConfig {
/// Enable the Media widget
pub enable: bool,

View File

@@ -1,14 +1,13 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use crate::widgets::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
@@ -17,7 +16,8 @@ use std::time::Instant;
use sysinfo::RefreshKind;
use sysinfo::System;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct MemoryConfig {
/// Enable the Memory widget
pub enable: bool,
@@ -25,6 +25,8 @@ pub struct MemoryConfig {
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
/// Select when the current percentage is over this value [[1-100]]
pub auto_select_over: Option<u8>,
}
impl From<MemoryConfig> for Memory {
@@ -38,6 +40,7 @@ impl From<MemoryConfig> for Memory {
),
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
last_updated: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
@@ -45,16 +48,23 @@ impl From<MemoryConfig> for Memory {
}
}
#[derive(Clone, Debug)]
struct MemoryOutput {
label: String,
selected: bool,
}
pub struct Memory {
pub enable: bool,
system: System,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
auto_select_over: Option<u8>,
last_updated: Instant,
}
impl Memory {
fn output(&mut self) -> String {
fn output(&mut self) -> MemoryOutput {
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.system.refresh_memory();
@@ -63,11 +73,17 @@ impl Memory {
let used = self.system.used_memory();
let total = self.system.total_memory();
match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("RAM: {}%", (used * 100) / total)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
let usage = ((used * 100) / total) as u8;
let selected = self.auto_select_over.is_some_and(|o| usage >= o);
MemoryOutput {
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("RAM: {}%", usage)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", usage),
},
selected,
}
}
}
@@ -76,7 +92,9 @@ impl BarWidget for Memory {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let output = self.output();
if !output.is_empty() {
if !output.label.is_empty() {
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
@@ -85,23 +103,25 @@ impl BarWidget for Memory {
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0,
);
layout_job.append(
&output,
&output.label,
10.0,
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
valign: Align::Center,
..Default::default()
},
);
let auto_focus_fill = config.auto_select_fill;
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{

View File

@@ -0,0 +1,13 @@
pub mod battery;
pub mod cpu;
pub mod date;
pub mod keyboard;
pub mod komorebi;
mod komorebi_layout;
pub mod media;
pub mod memory;
pub mod network;
pub mod storage;
pub mod time;
pub mod update;
pub mod widget;

View File

@@ -0,0 +1,540 @@
use crate::bar::Alignment;
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Color32;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use num_derive::FromPrimitive;
use serde::Deserialize;
use serde::Serialize;
use std::fmt;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
use sysinfo::Networks;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct NetworkConfig {
/// Enable the Network widget
pub enable: bool,
/// Show total received and transmitted activity
#[serde(alias = "show_total_data_transmitted")]
pub show_total_activity: bool,
/// Show received and transmitted activity
#[serde(alias = "show_network_activity")]
pub show_activity: bool,
/// Show default interface
pub show_default_interface: Option<bool>,
/// Characters to reserve for received and transmitted activity
#[serde(alias = "network_activity_fill_characters")]
pub activity_left_padding: Option<usize>,
/// Data refresh interval (default: 10 seconds)
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
/// Select when the value is over a limit (1MiB is 1048576 bytes (1024*1024))
pub auto_select: Option<NetworkSelectConfig>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct NetworkSelectConfig {
/// Select the total received data when it's over this value
pub total_received_over: Option<u64>,
/// Select the total transmitted data when it's over this value
pub total_transmitted_over: Option<u64>,
/// Select the received data when it's over this value
pub received_over: Option<u64>,
/// Select the transmitted data when it's over this value
pub transmitted_over: Option<u64>,
}
impl From<NetworkConfig> for Network {
fn from(value: NetworkConfig) -> Self {
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
Self {
enable: value.enable,
show_total_activity: value.show_total_activity,
show_activity: value.show_activity,
show_default_interface: value.show_default_interface.unwrap_or(true),
networks_network_activity: Networks::new_with_refreshed_list(),
default_interface: String::new(),
data_refresh_interval,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
auto_select: value.auto_select,
activity_left_padding: value.activity_left_padding.unwrap_or_default(),
last_state_total_activity: vec![],
last_state_activity: vec![],
last_updated_network_activity: Instant::now()
.checked_sub(Duration::from_secs(data_refresh_interval))
.unwrap(),
}
}
}
pub struct Network {
pub enable: bool,
pub show_total_activity: bool,
pub show_activity: bool,
pub show_default_interface: bool,
networks_network_activity: Networks,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
auto_select: Option<NetworkSelectConfig>,
default_interface: String,
last_state_total_activity: Vec<NetworkReading>,
last_state_activity: Vec<NetworkReading>,
last_updated_network_activity: Instant,
activity_left_padding: usize,
}
impl Network {
fn default_interface(&mut self) {
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
self.default_interface.clone_from(friendly_name);
}
}
}
fn network_activity(&mut self) -> (Vec<NetworkReading>, Vec<NetworkReading>) {
let mut activity = self.last_state_activity.clone();
let mut total_activity = self.last_state_total_activity.clone();
let now = Instant::now();
if now.duration_since(self.last_updated_network_activity)
> Duration::from_secs(self.data_refresh_interval)
{
activity.clear();
total_activity.clear();
if let Ok(interface) = netdev::get_default_interface() {
if let Some(friendly_name) = &interface.friendly_name {
self.default_interface.clone_from(friendly_name);
self.networks_network_activity.refresh(true);
for (interface_name, data) in &self.networks_network_activity {
if friendly_name.eq(interface_name) {
if self.show_activity {
let received = Self::to_pretty_bytes(
data.received(),
self.data_refresh_interval,
);
let transmitted = Self::to_pretty_bytes(
data.transmitted(),
self.data_refresh_interval,
);
activity.push(NetworkReading::new(
NetworkReadingFormat::Speed,
ReadingValue::from(received),
ReadingValue::from(transmitted),
));
}
if self.show_total_activity {
let total_received =
Self::to_pretty_bytes(data.total_received(), 1);
let total_transmitted =
Self::to_pretty_bytes(data.total_transmitted(), 1);
total_activity.push(NetworkReading::new(
NetworkReadingFormat::Total,
ReadingValue::from(total_received),
ReadingValue::from(total_transmitted),
))
}
}
}
}
}
self.last_state_activity.clone_from(&activity);
self.last_state_total_activity.clone_from(&total_activity);
self.last_updated_network_activity = now;
}
(activity, total_activity)
}
fn reading_to_labels(
&self,
select_received: bool,
select_transmitted: bool,
ctx: &Context,
reading: &NetworkReading,
config: RenderConfig,
) -> (Label, Label) {
let (text_down, text_up) = match self.label_prefix {
LabelPrefix::None | LabelPrefix::Icon => match reading.format {
NetworkReadingFormat::Speed => (
format!(
"{: >width$}/s ",
reading.received.pretty,
width = self.activity_left_padding
),
format!(
"{: >width$}/s",
reading.transmitted.pretty,
width = self.activity_left_padding
),
),
NetworkReadingFormat::Total => (
format!("{} ", reading.received.pretty),
reading.transmitted.pretty.clone(),
),
},
LabelPrefix::Text | LabelPrefix::IconAndText => match reading.format {
NetworkReadingFormat::Speed => (
format!(
"DOWN: {: >width$}/s ",
reading.received.pretty,
width = self.activity_left_padding
),
format!(
"UP: {: >width$}/s",
reading.transmitted.pretty,
width = self.activity_left_padding
),
),
NetworkReadingFormat::Total => (
format!("\u{2211}DOWN: {}/s ", reading.received.pretty),
format!("\u{2211}UP: {}/s", reading.transmitted.pretty),
),
},
};
let auto_text_color_received = config.auto_select_text.filter(|_| select_received);
let auto_text_color_transmitted = config.auto_select_text.filter(|_| select_transmitted);
// icon
let mut layout_job_down = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
if select_received {
egui_phosphor::regular::ARROW_FAT_LINES_DOWN.to_string()
} else {
egui_phosphor::regular::ARROW_FAT_DOWN.to_string()
}
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
auto_text_color_received.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0,
);
// text
layout_job_down.append(
&text_down,
ctx.style().spacing.item_spacing.x,
TextFormat {
font_id: config.text_font_id.clone(),
color: auto_text_color_received.unwrap_or(ctx.style().visuals.text_color()),
valign: Align::Center,
..Default::default()
},
);
// icon
let mut layout_job_up = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
if select_transmitted {
egui_phosphor::regular::ARROW_FAT_LINES_UP.to_string()
} else {
egui_phosphor::regular::ARROW_FAT_UP.to_string()
}
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
auto_text_color_transmitted.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0,
);
// text
layout_job_up.append(
&text_up,
ctx.style().spacing.item_spacing.x,
TextFormat {
font_id: config.text_font_id.clone(),
color: auto_text_color_transmitted.unwrap_or(ctx.style().visuals.text_color()),
valign: Align::Center,
..Default::default()
},
);
(
Label::new(layout_job_down).selectable(false),
Label::new(layout_job_up).selectable(false),
)
}
fn to_pretty_bytes(input_in_bytes: u64, timespan_in_s: u64) -> (u64, String) {
let input = input_in_bytes as f32 / timespan_in_s as f32;
let mut magnitude = input.log(1024f32) as u32;
// let the base unit be KiB
if magnitude < 1 {
magnitude = 1;
}
let base: Option<DataUnit> = num::FromPrimitive::from_u32(magnitude);
let result = input / ((1u64) << (magnitude * 10)) as f32;
(
input as u64,
match base {
Some(DataUnit::B) => format!("{result:.1} B"),
Some(unit) => format!("{result:.1} {unit}iB"),
None => String::from("Unknown data unit"),
},
)
}
fn show_frame<R>(
&self,
selected: bool,
auto_focus_fill: Option<Color32>,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) {
if SelectableFrame::new_auto(selected, auto_focus_fill)
.show(ui, add_contents)
.clicked()
{
if let Err(error) = Command::new("cmd.exe").args(["/C", "ncpa"]).spawn() {
eprintln!("{}", error);
}
}
}
}
impl BarWidget for Network {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
// widget spacing: make sure to use the same config to call the apply_on_widget function
let mut render_config = config.clone();
if self.show_total_activity || self.show_activity {
let (activity, total_activity) = self.network_activity();
if self.show_total_activity {
for reading in &total_activity {
render_config.apply_on_widget(false, ui, |ui| {
let select_received = self.auto_select.is_some_and(|f| {
f.total_received_over
.is_some_and(|o| reading.received.value > o)
});
let select_transmitted = self.auto_select.is_some_and(|f| {
f.total_transmitted_over
.is_some_and(|o| reading.transmitted.value > o)
});
let labels = self.reading_to_labels(
select_received,
select_transmitted,
ctx,
reading,
config.clone(),
);
if is_reversed {
self.show_frame(
select_transmitted,
config.auto_select_fill,
ui,
|ui| ui.add(labels.1),
);
self.show_frame(
select_received,
config.auto_select_fill,
ui,
|ui| ui.add(labels.0),
);
} else {
self.show_frame(
select_received,
config.auto_select_fill,
ui,
|ui| ui.add(labels.0),
);
self.show_frame(
select_transmitted,
config.auto_select_fill,
ui,
|ui| ui.add(labels.1),
);
}
});
}
}
if self.show_activity {
for reading in &activity {
render_config.apply_on_widget(false, ui, |ui| {
let select_received = self.auto_select.is_some_and(|f| {
f.received_over.is_some_and(|o| reading.received.value > o)
});
let select_transmitted = self.auto_select.is_some_and(|f| {
f.transmitted_over
.is_some_and(|o| reading.transmitted.value > o)
});
let labels = self.reading_to_labels(
select_received,
select_transmitted,
ctx,
reading,
config.clone(),
);
if is_reversed {
self.show_frame(
select_transmitted,
config.auto_select_fill,
ui,
|ui| ui.add(labels.1),
);
self.show_frame(
select_received,
config.auto_select_fill,
ui,
|ui| ui.add(labels.0),
);
} else {
self.show_frame(
select_received,
config.auto_select_fill,
ui,
|ui| ui.add(labels.0),
);
self.show_frame(
select_transmitted,
config.auto_select_fill,
ui,
|ui| ui.add(labels.1),
);
}
});
}
}
}
if self.show_default_interface {
self.default_interface();
if !self.default_interface.is_empty() {
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::WIFI_HIGH.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
self.default_interface.insert_str(0, "NET: ");
}
layout_job.append(
&self.default_interface,
10.0,
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
},
);
render_config.apply_on_widget(false, ui, |ui| {
self.show_frame(false, None, ui, |ui| {
ui.add(Label::new(layout_job).selectable(false))
});
});
}
}
// widget spacing: pass on the config that was use for calling the apply_on_widget function
*config = render_config.clone();
}
}
}
#[derive(Clone)]
enum NetworkReadingFormat {
Speed = 0,
Total = 1,
}
#[derive(Clone)]
struct ReadingValue {
value: u64,
pretty: String,
}
impl From<(u64, String)> for ReadingValue {
fn from(value: (u64, String)) -> Self {
Self {
value: value.0,
pretty: value.1,
}
}
}
#[derive(Clone)]
struct NetworkReading {
format: NetworkReadingFormat,
received: ReadingValue,
transmitted: ReadingValue,
}
impl NetworkReading {
fn new(
format: NetworkReadingFormat,
received: ReadingValue,
transmitted: ReadingValue,
) -> Self {
Self {
format,
received,
transmitted,
}
}
}
#[derive(Debug, FromPrimitive)]
enum DataUnit {
B = 0,
K = 1,
M = 2,
G = 3,
T = 4,
P = 5,
E = 6,
Z = 7,
Y = 8,
}
impl fmt::Display for DataUnit {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}

View File

@@ -1,14 +1,14 @@
use crate::bar::Alignment;
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use crate::widgets::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
@@ -16,7 +16,8 @@ use std::time::Duration;
use std::time::Instant;
use sysinfo::Disks;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct StorageConfig {
/// Enable the Storage widget
pub enable: bool,
@@ -24,6 +25,10 @@ pub struct StorageConfig {
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
/// 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]]
pub auto_hide_under: Option<u8>,
}
impl From<StorageConfig> for Storage {
@@ -33,21 +38,30 @@ 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),
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(),
}
}
}
struct StorageDisk {
label: String,
selected: bool,
}
pub struct Storage {
pub enable: bool,
disks: Disks,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
auto_select_over: Option<u8>,
auto_hide_under: Option<u8>,
last_updated: Instant,
}
impl Storage {
fn output(&mut self) -> Vec<String> {
fn output(&mut self) -> Vec<StorageDisk> {
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.disks.refresh(true);
@@ -61,17 +75,26 @@ impl Storage {
let total = disk.total_space();
let available = disk.available_space();
let used = total - available;
let percentage = ((used * 100) / total) as u8;
disks.push(match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("{} {}%", mount.to_string_lossy(), (used * 100) / total)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", (used * 100) / total),
})
let hide = self.auto_hide_under.is_some_and(|u| percentage <= u);
if !hide {
let selected = self.auto_select_over.is_some_and(|o| percentage >= o);
disks.push(StorageDisk {
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("{} {}%", mount.to_string_lossy(), percentage)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{}%", percentage),
},
selected,
})
}
}
disks.sort();
disks.reverse();
disks.sort_by(|a, b| a.label.cmp(&b.label));
disks
}
@@ -80,7 +103,16 @@ impl Storage {
impl BarWidget for Storage {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
for output in self.output() {
let mut output = self.output();
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
if is_reversed {
output.reverse();
}
for output in output {
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
@@ -89,23 +121,25 @@ impl BarWidget for Storage {
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0,
);
layout_job.append(
&output,
&output.label,
10.0,
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
valign: Align::Center,
..Default::default()
},
);
let auto_focus_fill = config.auto_select_fill;
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{
@@ -113,7 +147,7 @@ impl BarWidget for Storage {
.args([
"/C",
"explorer.exe",
output.split(' ').collect::<Vec<&str>>()[0],
output.label.split(' ').collect::<Vec<&str>>()[0],
])
.spawn()
{

View File

@@ -2,7 +2,10 @@ use crate::bar::Alignment;
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use crate::widgets::widget::BarWidget;
use chrono::Local;
use chrono::NaiveTime;
use chrono_tz::Tz;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
@@ -14,11 +17,61 @@ use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use eframe::epaint::StrokeKind;
use schemars::JsonSchema;
use lazy_static::lazy_static;
use serde::Deserialize;
use serde::Serialize;
use std::time::Duration;
use std::time::Instant;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
lazy_static! {
static ref TIME_RANGES: Vec<(&'static str, NaiveTime)> = {
vec![
(
egui_phosphor::regular::MOON,
NaiveTime::from_hms_opt(0, 0, 0).expect("invalid"),
),
(
egui_phosphor::regular::ALARM,
NaiveTime::from_hms_opt(6, 0, 0).expect("invalid"),
),
(
egui_phosphor::regular::BREAD,
NaiveTime::from_hms_opt(6, 1, 0).expect("invalid"),
),
(
egui_phosphor::regular::BARBELL,
NaiveTime::from_hms_opt(6, 30, 0).expect("invalid"),
),
(
egui_phosphor::regular::COFFEE,
NaiveTime::from_hms_opt(8, 0, 0).expect("invalid"),
),
(
egui_phosphor::regular::CLOCK,
NaiveTime::from_hms_opt(8, 30, 0).expect("invalid"),
),
(
egui_phosphor::regular::HAMBURGER,
NaiveTime::from_hms_opt(12, 0, 0).expect("invalid"),
),
(
egui_phosphor::regular::CLOCK_AFTERNOON,
NaiveTime::from_hms_opt(12, 30, 0).expect("invalid"),
),
(
egui_phosphor::regular::FORK_KNIFE,
NaiveTime::from_hms_opt(18, 0, 0).expect("invalid"),
),
(
egui_phosphor::regular::MOON_STARS,
NaiveTime::from_hms_opt(18, 30, 0).expect("invalid"),
),
]
};
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct TimeConfig {
/// Enable the Time widget
pub enable: bool,
@@ -26,19 +79,46 @@ pub struct TimeConfig {
pub format: TimeFormat,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
/// TimeZone (https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html)
///
/// Use a custom format to display additional information, i.e.:
/// ```json
/// {
/// "Time": {
/// "enable": true,
/// "format": { "Custom": "%T %Z (Tokyo)" },
/// "timezone": "Asia/Tokyo"
/// }
///}
/// ```
pub timezone: Option<String>,
/// Change the icon depending on the time. The default icon is used between 8:30 and 12:00. (default: false)
pub changing_icon: Option<bool>,
}
impl From<TimeConfig> for Time {
fn from(value: TimeConfig) -> Self {
// using 1 second made the widget look "less accurate" and lagging (especially having multiple with seconds).
// This is still better than getting an update every frame
let data_refresh_interval = 500;
Self {
enable: value.enable,
format: value.format,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
timezone: value.timezone,
changing_icon: value.changing_icon.unwrap_or_default(),
data_refresh_interval_millis: data_refresh_interval,
last_state: TimeOutput::new(),
last_updated: Instant::now()
.checked_sub(Duration::from_millis(data_refresh_interval))
.unwrap(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum TimeFormat {
/// Twelve-hour format (with seconds)
TwelveHour,
@@ -82,20 +162,94 @@ impl TimeFormat {
}
}
#[derive(Clone, Debug)]
struct TimeOutput {
label: String,
icon: String,
}
impl TimeOutput {
fn new() -> Self {
Self {
label: String::new(),
icon: String::new(),
}
}
}
#[derive(Clone, Debug)]
pub struct Time {
pub enable: bool,
pub format: TimeFormat,
label_prefix: LabelPrefix,
timezone: Option<String>,
changing_icon: bool,
data_refresh_interval_millis: u64,
last_state: TimeOutput,
last_updated: Instant,
}
impl Time {
fn output(&mut self) -> String {
chrono::Local::now()
.format(&self.format.fmt_string())
.to_string()
.trim()
.to_string()
fn output(&mut self) -> TimeOutput {
let mut output = self.last_state.clone();
let now = Instant::now();
if now.duration_since(self.last_updated)
> Duration::from_millis(self.data_refresh_interval_millis)
{
let (formatted, current_time) = match &self.timezone {
Some(timezone) => match timezone.parse::<Tz>() {
Ok(tz) => {
let dt = Local::now().with_timezone(&tz);
(
dt.format(&self.format.fmt_string())
.to_string()
.trim()
.to_string(),
Some(dt.time()),
)
}
Err(_) => (format!("Invalid timezone: {:?}", timezone), None),
},
None => {
let dt = Local::now();
(
dt.format(&self.format.fmt_string())
.to_string()
.trim()
.to_string(),
Some(dt.time()),
)
}
};
if current_time.is_none() {
return TimeOutput {
label: formatted,
icon: egui_phosphor::regular::WARNING_CIRCLE.to_string(),
};
}
let current_range = match &self.changing_icon {
true => TIME_RANGES
.iter()
.rev()
.find(|&(_, start)| current_time.unwrap() > *start)
.cloned(),
false => None,
}
.unwrap_or((egui_phosphor::regular::CLOCK, NaiveTime::default()));
output = TimeOutput {
label: formatted,
icon: current_range.0.to_string(),
};
self.last_state.clone_from(&output);
self.last_updated = now;
}
output
}
fn paint_binary_circle(
@@ -109,36 +263,37 @@ impl Time {
let full_height = size;
let height = full_height / 4.0;
let width = height;
let offset = height / 2.0 - height / 8.0;
let (response, painter) =
ui.allocate_painter(Vec2::new(width, full_height), Sense::hover());
ui.allocate_painter(Vec2::new(width, full_height + offset * 2.0), Sense::hover());
let color = ctx.style().visuals.text_color();
let c = response.rect.center();
let r = height / 2.0 - 0.5;
if number == 1 || number == 3 || number == 5 || number == 7 || number == 9 {
painter.circle_filled(c + Vec2::new(0.0, height * 1.50), r, color);
painter.circle_filled(c + Vec2::new(0.0, height * 1.50 + offset), r, color);
} else {
painter.circle_filled(c + Vec2::new(0.0, height * 1.50), r / 2.5, color);
painter.circle_filled(c + Vec2::new(0.0, height * 1.50 + offset), r / 2.5, color);
}
if number == 2 || number == 3 || number == 6 || number == 7 {
painter.circle_filled(c + Vec2::new(0.0, height * 0.50), r, color);
painter.circle_filled(c + Vec2::new(0.0, height * 0.50 + offset), r, color);
} else {
painter.circle_filled(c + Vec2::new(0.0, height * 0.50), r / 2.5, color);
painter.circle_filled(c + Vec2::new(0.0, height * 0.50 + offset), r / 2.5, color);
}
if number == 4 || number == 5 || number == 6 || number == 7 {
painter.circle_filled(c + Vec2::new(0.0, -height * 0.50), r, color);
painter.circle_filled(c + Vec2::new(0.0, -height * 0.50 + offset), r, color);
} else if max_power > 2 {
painter.circle_filled(c + Vec2::new(0.0, -height * 0.50), r / 2.5, color);
painter.circle_filled(c + Vec2::new(0.0, -height * 0.50 + offset), r / 2.5, color);
}
if number == 8 || number == 9 {
painter.circle_filled(c + Vec2::new(0.0, -height * 1.50), r, color);
painter.circle_filled(c + Vec2::new(0.0, -height * 1.50 + offset), r, color);
} else if max_power > 3 {
painter.circle_filled(c + Vec2::new(0.0, -height * 1.50), r / 2.5, color);
painter.circle_filled(c + Vec2::new(0.0, -height * 1.50 + offset), r / 2.5, color);
}
}
@@ -153,9 +308,10 @@ impl Time {
let full_height = size;
let height = full_height / 4.0;
let width = height * 1.5;
let offset = height / 2.0 - height / 8.0;
let (response, painter) =
ui.allocate_painter(Vec2::new(width, full_height), Sense::hover());
ui.allocate_painter(Vec2::new(width, full_height + offset * 2.0), Sense::hover());
let color = ctx.style().visuals.text_color();
let stroke = Stroke::new(1.0, color);
@@ -173,22 +329,25 @@ impl Time {
};
if max_power == 2 {
let mut rect = response.rect.shrink(stroke.width);
let mut rect = response
.rect
.shrink2(Vec2::new(stroke.width, stroke.width + offset));
rect.set_height(rect.height() - height * 2.0);
rect = rect.translate(Vec2::new(0.0, height * 2.0));
rect = rect.translate(Vec2::new(0.0, height * 2.0 + offset));
painter.rect_stroke(rect, round_all, stroke, StrokeKind::Outside);
} else if max_power == 3 {
let mut rect = response.rect.shrink(stroke.width);
let mut rect = response
.rect
.shrink2(Vec2::new(stroke.width, stroke.width + offset));
rect.set_height(rect.height() - height);
rect = rect.translate(Vec2::new(0.0, height));
rect = rect.translate(Vec2::new(0.0, height + offset));
painter.rect_stroke(rect, round_all, stroke, StrokeKind::Outside);
} else {
painter.rect_stroke(
response.rect.shrink(stroke.width),
round_all,
stroke,
StrokeKind::Outside,
);
let mut rect = response
.rect
.shrink2(Vec2::new(stroke.width, stroke.width + offset));
rect = rect.translate(Vec2::new(0.0, 0.0 + offset));
painter.rect_stroke(rect, round_all, stroke, StrokeKind::Outside);
}
let mut rect_bin = response.rect;
@@ -197,7 +356,7 @@ impl Time {
if number == 1 || number == 5 || number == 9 {
rect_bin.set_height(height);
painter.rect_filled(
rect_bin.translate(Vec2::new(0.0, height * 3.0)),
rect_bin.translate(Vec2::new(stroke.width, height * 3.0 + offset * 2.0)),
round_bottom,
color,
);
@@ -205,7 +364,7 @@ impl Time {
if number == 2 {
rect_bin.set_height(height);
painter.rect_filled(
rect_bin.translate(Vec2::new(0.0, height * 2.0)),
rect_bin.translate(Vec2::new(stroke.width, height * 2.0 + offset * 2.0)),
if max_power == 2 {
round_top
} else {
@@ -217,7 +376,7 @@ impl Time {
if number == 3 {
rect_bin.set_height(height * 2.0);
painter.rect_filled(
rect_bin.translate(Vec2::new(0.0, height * 2.0)),
rect_bin.translate(Vec2::new(stroke.width, height * 2.0 + offset * 2.0)),
round_bottom,
color,
);
@@ -225,7 +384,7 @@ impl Time {
if number == 4 || number == 5 {
rect_bin.set_height(height);
painter.rect_filled(
rect_bin.translate(Vec2::new(0.0, height * 1.0)),
rect_bin.translate(Vec2::new(stroke.width, height * 1.0 + offset * 2.0)),
if max_power == 3 {
round_top
} else {
@@ -237,7 +396,7 @@ impl Time {
if number == 6 {
rect_bin.set_height(height * 2.0);
painter.rect_filled(
rect_bin.translate(Vec2::new(0.0, height * 1.0)),
rect_bin.translate(Vec2::new(stroke.width, height * 1.0 + offset * 2.0)),
if max_power == 3 {
round_top
} else {
@@ -249,7 +408,7 @@ impl Time {
if number == 7 {
rect_bin.set_height(height * 3.0);
painter.rect_filled(
rect_bin.translate(Vec2::new(0.0, height)),
rect_bin.translate(Vec2::new(stroke.width, height + offset * 2.0)),
if max_power == 3 {
round_all
} else {
@@ -260,7 +419,11 @@ impl Time {
}
if number == 8 || number == 9 {
rect_bin.set_height(height);
painter.rect_filled(rect_bin.translate(Vec2::new(0.0, 0.0)), round_top, color);
painter.rect_filled(
rect_bin.translate(Vec2::new(stroke.width, 0.0 + offset * 2.0)),
round_top,
color,
);
}
}
}
@@ -269,15 +432,13 @@ impl BarWidget for Time {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let mut output = self.output();
if !output.is_empty() {
let use_binary_circle = output.starts_with('c');
let use_binary_rectangle = output.starts_with('r');
if !output.label.is_empty() {
let use_binary_circle = output.label.starts_with('c');
let use_binary_rectangle = output.label.starts_with('r');
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::CLOCK.to_string()
}
LabelPrefix::Icon | LabelPrefix::IconAndText => output.icon,
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
@@ -286,12 +447,12 @@ impl BarWidget for Time {
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
output.insert_str(0, "TIME: ");
output.label.insert_str(0, "TIME: ");
}
if !use_binary_circle && !use_binary_rectangle {
layout_job.append(
&output,
&output.label,
10.0,
TextFormat {
font_id: config.text_font_id.clone(),
@@ -314,9 +475,9 @@ impl BarWidget for Time {
if use_binary_circle || use_binary_rectangle {
let ordered_output = if is_reversed {
output.chars().rev().collect()
output.label.chars().rev().collect()
} else {
output
output.label
};
for (section_index, section) in

View File

@@ -1,21 +1,21 @@
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widget::BarWidget;
use crate::widgets::widget::BarWidget;
use eframe::egui::text::LayoutJob;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct UpdateConfig {
/// Enable the Update widget
pub enable: bool,

View File

@@ -1,29 +1,28 @@
use crate::battery::Battery;
use crate::battery::BatteryConfig;
use crate::cpu::Cpu;
use crate::cpu::CpuConfig;
use crate::date::Date;
use crate::date::DateConfig;
use crate::keyboard::Keyboard;
use crate::keyboard::KeyboardConfig;
use crate::komorebi::Komorebi;
use crate::komorebi::KomorebiConfig;
use crate::media::Media;
use crate::media::MediaConfig;
use crate::memory::Memory;
use crate::memory::MemoryConfig;
use crate::network::Network;
use crate::network::NetworkConfig;
use crate::render::RenderConfig;
use crate::storage::Storage;
use crate::storage::StorageConfig;
use crate::time::Time;
use crate::time::TimeConfig;
use crate::update::Update;
use crate::update::UpdateConfig;
use crate::widgets::battery::Battery;
use crate::widgets::battery::BatteryConfig;
use crate::widgets::cpu::Cpu;
use crate::widgets::cpu::CpuConfig;
use crate::widgets::date::Date;
use crate::widgets::date::DateConfig;
use crate::widgets::keyboard::Keyboard;
use crate::widgets::keyboard::KeyboardConfig;
use crate::widgets::komorebi::Komorebi;
use crate::widgets::komorebi::KomorebiConfig;
use crate::widgets::media::Media;
use crate::widgets::media::MediaConfig;
use crate::widgets::memory::Memory;
use crate::widgets::memory::MemoryConfig;
use crate::widgets::network::Network;
use crate::widgets::network::NetworkConfig;
use crate::widgets::storage::Storage;
use crate::widgets::storage::StorageConfig;
use crate::widgets::time::Time;
use crate::widgets::time::TimeConfig;
use crate::widgets::update::Update;
use crate::widgets::update::UpdateConfig;
use eframe::egui::Context;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
@@ -31,7 +30,8 @@ pub trait BarWidget {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig);
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum WidgetConfig {
Battery(BatteryConfig),
Cpu(CpuConfig),
@@ -72,7 +72,7 @@ impl WidgetConfig {
WidgetConfig::Komorebi(config) => {
config.workspaces.as_ref().is_some_and(|w| w.enable)
|| config.layout.as_ref().is_some_and(|w| w.enable)
|| config.focused_window.as_ref().is_some_and(|w| w.enable)
|| config.focused_container.as_ref().is_some_and(|w| w.enable)
|| config
.configuration_switcher
.as_ref()

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-client"
version = "0.1.35"
version = "0.1.36"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -10,3 +10,7 @@ komorebi = { path = "../komorebi" }
uds_windows = { workspace = true }
serde_json = { workspace = true }
[features]
default = ["schemars"]
schemars = ["komorebi/schemars"]

View File

@@ -4,8 +4,7 @@
pub use komorebi::animation::prefix::AnimationPrefix;
pub use komorebi::animation::PerAnimationPrefixConfig;
pub use komorebi::asc::ApplicationSpecificConfiguration;
pub use komorebi::colour::Colour;
pub use komorebi::colour::Rgb;
pub use komorebi::border_manager::BorderInfo;
pub use komorebi::config_generation::ApplicationConfiguration;
pub use komorebi::config_generation::IdWithIdentifier;
pub use komorebi::config_generation::IdWithIdentifierAndComment;
@@ -13,7 +12,7 @@ pub use komorebi::config_generation::MatchingRule;
pub use komorebi::config_generation::MatchingStrategy;
pub use komorebi::container::Container;
pub use komorebi::core::config_generation::ApplicationConfigurationGenerator;
pub use komorebi::core::resolve_home_path;
pub use komorebi::core::replace_env_in_path;
pub use komorebi::core::AnimationStyle;
pub use komorebi::core::ApplicationIdentifier;
pub use komorebi::core::Arrangement;
@@ -28,6 +27,7 @@ pub use komorebi::core::CustomLayout;
pub use komorebi::core::CycleDirection;
pub use komorebi::core::DefaultLayout;
pub use komorebi::core::Direction;
pub use komorebi::core::FloatingLayerBehaviour;
pub use komorebi::core::FocusFollowsMouseImplementation;
pub use komorebi::core::HidingBehaviour;
pub use komorebi::core::Layout;
@@ -45,14 +45,17 @@ pub use komorebi::core::WindowKind;
pub use komorebi::monitor::Monitor;
pub use komorebi::monitor_reconciliator::MonitorNotification;
pub use komorebi::ring::Ring;
pub use komorebi::win32_display_data;
pub use komorebi::window::Window;
pub use komorebi::window_manager_event::WindowManagerEvent;
pub use komorebi::workspace::Workspace;
pub use komorebi::workspace::WorkspaceGlobals;
pub use komorebi::workspace::WorkspaceLayer;
pub use komorebi::AnimationsConfig;
pub use komorebi::AppSpecificConfigurationPath;
pub use komorebi::AspectRatio;
pub use komorebi::BorderColours;
pub use komorebi::Colour;
pub use komorebi::CrossBoundaryBehaviour;
pub use komorebi::GlobalState;
pub use komorebi::KomorebiTheme;
@@ -60,6 +63,7 @@ pub use komorebi::MonitorConfig;
pub use komorebi::Notification;
pub use komorebi::NotificationEvent;
pub use komorebi::PredefinedAspectRatio;
pub use komorebi::Rgb;
pub use komorebi::RuleDebug;
pub use komorebi::StackbarConfig;
pub use komorebi::State;

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-gui"
version = "0.1.35"
version = "0.1.36"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -10,7 +10,7 @@ komorebi-client = { path = "../komorebi-client" }
eframe = { workspace = true }
egui_extras = { workspace = true }
random_word = { version = "0.4", features = ["en"] }
random_word = { version = "0.5", features = ["en"] }
serde_json = { workspace = true }
windows-core = { workspace = true }
windows = { workspace = true }

View File

@@ -41,7 +41,9 @@ struct BorderColours {
single: Color32,
stack: Color32,
monocle: Color32,
floating: Color32,
unfocused: Color32,
unfocused_locked: Color32,
}
struct BorderConfig {
@@ -101,7 +103,7 @@ impl From<&komorebi_client::Workspace> for WorkspaceConfig {
let name = value
.name()
.to_owned()
.unwrap_or_else(|| random_word::gen(random_word::Lang::En).to_string());
.unwrap_or_else(|| random_word::get(random_word::Lang::En).to_string());
Self {
layout,
@@ -154,7 +156,9 @@ impl KomorebiGui {
single: colour32(global_state.border_colours.single),
stack: colour32(global_state.border_colours.stack),
monocle: colour32(global_state.border_colours.monocle),
floating: colour32(global_state.border_colours.floating),
unfocused: colour32(global_state.border_colours.unfocused),
unfocused_locked: colour32(global_state.border_colours.unfocused_locked),
};
let border_config = BorderConfig {
@@ -377,6 +381,22 @@ impl eframe::App for KomorebiGui {
}
});
ui.collapsing("Floating", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.border_config.border_colours.floating,
Alpha::Opaque,
) {
komorebi_client::send_message(&SocketMessage::BorderColour(
WindowKind::Floating,
self.border_config.border_colours.floating.r() as u32,
self.border_config.border_colours.floating.g() as u32,
self.border_config.border_colours.floating.b() as u32,
))
.unwrap();
}
});
ui.collapsing("Unfocused", |ui| {
if egui::color_picker::color_picker_color32(
ui,
@@ -391,6 +411,22 @@ impl eframe::App for KomorebiGui {
))
.unwrap();
}
});
ui.collapsing("Unfocused Locked", |ui| {
if egui::color_picker::color_picker_color32(
ui,
&mut self.border_config.border_colours.unfocused_locked,
Alpha::Opaque,
) {
komorebi_client::send_message(&SocketMessage::BorderColour(
WindowKind::UnfocusedLocked,
self.border_config.border_colours.unfocused_locked.r() as u32,
self.border_config.border_colours.unfocused_locked.g() as u32,
self.border_config.border_colours.unfocused_locked.b() as u32,
))
.unwrap();
}
})
});

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi-themes"
version = "0.1.35"
version = "0.1.36"
edition = "2021"
[dependencies]
@@ -8,7 +8,13 @@ base16-egui-themes = { git = "https://github.com/LGUG2Z/base16-egui-themes", rev
catppuccin-egui = { git = "https://github.com/LGUG2Z/catppuccin-egui", rev = "bdaff30959512c4f7ee7304117076a48633d777f", default-features = false, features = ["egui31"] }
#catppuccin-egui = { version = "5", default-features = false, features = ["egui30"] }
eframe = { workspace = true }
schemars = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true }
serde_variant = "0.1"
strum = { workspace = true }
hex_color = { version = "3", features = ["serde"] }
flavours = { git = "https://github.com/LGUG2Z/flavours", version = "0.7.2" }
[features]
default = ["schemars"]
schemars = ["dep:schemars"]

View File

@@ -1,14 +1,19 @@
use hex_color::HexColor;
use komorebi_themes::Color32;
#[cfg(feature = "schemars")]
use schemars::gen::SchemaGenerator;
#[cfg(feature = "schemars")]
use schemars::schema::InstanceType;
#[cfg(feature = "schemars")]
use schemars::schema::Schema;
#[cfg(feature = "schemars")]
use schemars::schema::SchemaObject;
use schemars::JsonSchema;
use crate::Color32;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum Colour {
/// Colour represented as RGB
@@ -52,9 +57,10 @@ impl From<Colour> for Color32 {
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
pub struct Hex(HexColor);
pub struct Hex(pub HexColor);
impl JsonSchema for Hex {
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for Hex {
fn schema_name() -> String {
String::from("Hex")
}
@@ -78,7 +84,8 @@ impl From<Colour> for u32 {
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Rgb {
/// Red
pub r: u32,

View File

@@ -0,0 +1,77 @@
use crate::colour::Colour;
use crate::colour::Hex;
use crate::Base16ColourPalette;
use hex_color::HexColor;
use std::collections::VecDeque;
use std::fmt::Display;
use std::fmt::Formatter;
use std::path::Path;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum ThemeVariant {
#[default]
Dark,
Light,
}
impl Display for ThemeVariant {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ThemeVariant::Dark => write!(f, "dark"),
ThemeVariant::Light => write!(f, "light"),
}
}
}
impl From<ThemeVariant> for flavours::operations::generate::Mode {
fn from(value: ThemeVariant) -> Self {
match value {
ThemeVariant::Dark => Self::Dark,
ThemeVariant::Light => Self::Light,
}
}
}
pub fn generate_base16_palette(
image_path: &Path,
variant: ThemeVariant,
) -> Result<Base16ColourPalette, hex_color::ParseHexColorError> {
Base16ColourPalette::try_from(
&flavours::operations::generate::generate(image_path, variant.into(), false)
.unwrap_or_default(),
)
}
impl TryFrom<&VecDeque<String>> for Base16ColourPalette {
type Error = hex_color::ParseHexColorError;
fn try_from(value: &VecDeque<String>) -> Result<Self, Self::Error> {
let fixed = value.iter().map(|s| format!("#{s}")).collect::<Vec<_>>();
if fixed.len() != 16 {
return Err(hex_color::ParseHexColorError::Empty);
}
Ok(Self {
base_00: Colour::Hex(Hex(HexColor::parse(&fixed[0])?)),
base_01: Colour::Hex(Hex(HexColor::parse(&fixed[1])?)),
base_02: Colour::Hex(Hex(HexColor::parse(&fixed[2])?)),
base_03: Colour::Hex(Hex(HexColor::parse(&fixed[3])?)),
base_04: Colour::Hex(Hex(HexColor::parse(&fixed[4])?)),
base_05: Colour::Hex(Hex(HexColor::parse(&fixed[5])?)),
base_06: Colour::Hex(Hex(HexColor::parse(&fixed[6])?)),
base_07: Colour::Hex(Hex(HexColor::parse(&fixed[7])?)),
base_08: Colour::Hex(Hex(HexColor::parse(&fixed[8])?)),
base_09: Colour::Hex(Hex(HexColor::parse(&fixed[9])?)),
base_0a: Colour::Hex(Hex(HexColor::parse(&fixed[10])?)),
base_0b: Colour::Hex(Hex(HexColor::parse(&fixed[11])?)),
base_0c: Colour::Hex(Hex(HexColor::parse(&fixed[12])?)),
base_0d: Colour::Hex(Hex(HexColor::parse(&fixed[13])?)),
base_0e: Colour::Hex(Hex(HexColor::parse(&fixed[14])?)),
base_0f: Colour::Hex(Hex(HexColor::parse(&fixed[15])?)),
})
}
}

View File

@@ -1,18 +1,32 @@
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc)]
pub mod colour;
mod generator;
pub use generator::generate_base16_palette;
pub use generator::ThemeVariant;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::IntoEnumIterator;
use crate::colour::Colour;
pub use base16_egui_themes::Base16;
pub use catppuccin_egui;
use eframe::egui::style::Selection;
use eframe::egui::style::WidgetVisuals;
use eframe::egui::style::Widgets;
pub use eframe::egui::Color32;
use eframe::egui::Shadow;
use eframe::egui::Stroke;
use eframe::egui::Style;
use eframe::egui::Visuals;
use serde_variant::to_variant_name;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "type")]
pub enum Theme {
/// A theme from catppuccin-egui
@@ -25,6 +39,140 @@ pub enum Theme {
name: Base16,
accent: Option<Base16Value>,
},
/// A custom base16 palette
Custom {
palette: Box<Base16ColourPalette>,
accent: Option<Base16Value>,
},
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct Base16ColourPalette {
pub base_00: Colour,
pub base_01: Colour,
pub base_02: Colour,
pub base_03: Colour,
pub base_04: Colour,
pub base_05: Colour,
pub base_06: Colour,
pub base_07: Colour,
pub base_08: Colour,
pub base_09: Colour,
pub base_0a: Colour,
pub base_0b: Colour,
pub base_0c: Colour,
pub base_0d: Colour,
pub base_0e: Colour,
pub base_0f: Colour,
}
impl Base16ColourPalette {
pub fn background(self) -> Color32 {
self.base_01.into()
}
pub fn style(self) -> Style {
let original = Style::default();
Style {
visuals: Visuals {
widgets: Widgets {
noninteractive: WidgetVisuals {
bg_fill: self.base_01.into(),
weak_bg_fill: self.base_01.into(),
bg_stroke: Stroke {
color: self.base_02.into(),
..original.visuals.widgets.noninteractive.bg_stroke
},
fg_stroke: Stroke {
color: self.base_05.into(),
..original.visuals.widgets.noninteractive.fg_stroke
},
..original.visuals.widgets.noninteractive
},
inactive: WidgetVisuals {
bg_fill: self.base_02.into(),
weak_bg_fill: self.base_02.into(),
bg_stroke: Stroke {
color: Color32::from_rgba_premultiplied(0, 0, 0, 0),
..original.visuals.widgets.inactive.bg_stroke
},
fg_stroke: Stroke {
color: self.base_05.into(),
..original.visuals.widgets.inactive.fg_stroke
},
..original.visuals.widgets.inactive
},
hovered: WidgetVisuals {
bg_fill: self.base_02.into(),
weak_bg_fill: self.base_02.into(),
bg_stroke: Stroke {
color: self.base_03.into(),
..original.visuals.widgets.hovered.bg_stroke
},
fg_stroke: Stroke {
color: self.base_06.into(),
..original.visuals.widgets.hovered.fg_stroke
},
..original.visuals.widgets.hovered
},
active: WidgetVisuals {
bg_fill: self.base_02.into(),
weak_bg_fill: self.base_02.into(),
bg_stroke: Stroke {
color: self.base_03.into(),
..original.visuals.widgets.hovered.bg_stroke
},
fg_stroke: Stroke {
color: self.base_06.into(),
..original.visuals.widgets.hovered.fg_stroke
},
..original.visuals.widgets.active
},
open: WidgetVisuals {
bg_fill: self.base_01.into(),
weak_bg_fill: self.base_01.into(),
bg_stroke: Stroke {
color: self.base_02.into(),
..original.visuals.widgets.open.bg_stroke
},
fg_stroke: Stroke {
color: self.base_06.into(),
..original.visuals.widgets.open.fg_stroke
},
..original.visuals.widgets.open
},
},
selection: Selection {
bg_fill: self.base_02.into(),
stroke: Stroke {
color: self.base_06.into(),
..original.visuals.selection.stroke
},
},
hyperlink_color: self.base_08.into(),
faint_bg_color: Color32::from_rgba_premultiplied(0, 0, 0, 0),
extreme_bg_color: self.base_00.into(),
code_bg_color: self.base_02.into(),
warn_fg_color: self.base_0c.into(),
error_fg_color: self.base_0b.into(),
window_shadow: Shadow {
color: Color32::from_rgba_premultiplied(0, 0, 0, 96),
..original.visuals.window_shadow
},
window_fill: self.base_01.into(),
window_stroke: Stroke {
color: self.base_02.into(),
..original.visuals.window_stroke
},
panel_fill: self.base_01.into(),
popup_shadow: Shadow {
color: Color32::from_rgba_premultiplied(0, 0, 0, 96),
..original.visuals.popup_shadow
},
..original.visuals
},
..original
}
}
}
impl Theme {
@@ -45,6 +193,7 @@ impl Theme {
.to_string()
})
.collect(),
Theme::Custom { .. } => vec!["Custom".to_string()],
}
}
}
@@ -70,25 +219,50 @@ pub enum Base16Value {
Base0F,
}
pub enum Base16Wrapper {
Base16(Base16),
Custom(Box<Base16ColourPalette>),
}
impl Base16Value {
pub fn color32(&self, theme: Base16) -> Color32 {
match self {
Base16Value::Base00 => theme.base00(),
Base16Value::Base01 => theme.base01(),
Base16Value::Base02 => theme.base02(),
Base16Value::Base03 => theme.base03(),
Base16Value::Base04 => theme.base04(),
Base16Value::Base05 => theme.base05(),
Base16Value::Base06 => theme.base06(),
Base16Value::Base07 => theme.base07(),
Base16Value::Base08 => theme.base08(),
Base16Value::Base09 => theme.base09(),
Base16Value::Base0A => theme.base0a(),
Base16Value::Base0B => theme.base0b(),
Base16Value::Base0C => theme.base0c(),
Base16Value::Base0D => theme.base0d(),
Base16Value::Base0E => theme.base0e(),
Base16Value::Base0F => theme.base0f(),
pub fn color32(&self, theme: Base16Wrapper) -> Color32 {
match theme {
Base16Wrapper::Base16(theme) => match self {
Base16Value::Base00 => theme.base00(),
Base16Value::Base01 => theme.base01(),
Base16Value::Base02 => theme.base02(),
Base16Value::Base03 => theme.base03(),
Base16Value::Base04 => theme.base04(),
Base16Value::Base05 => theme.base05(),
Base16Value::Base06 => theme.base06(),
Base16Value::Base07 => theme.base07(),
Base16Value::Base08 => theme.base08(),
Base16Value::Base09 => theme.base09(),
Base16Value::Base0A => theme.base0a(),
Base16Value::Base0B => theme.base0b(),
Base16Value::Base0C => theme.base0c(),
Base16Value::Base0D => theme.base0d(),
Base16Value::Base0E => theme.base0e(),
Base16Value::Base0F => theme.base0f(),
},
Base16Wrapper::Custom(colours) => match self {
Base16Value::Base00 => colours.base_00.into(),
Base16Value::Base01 => colours.base_01.into(),
Base16Value::Base02 => colours.base_02.into(),
Base16Value::Base03 => colours.base_03.into(),
Base16Value::Base04 => colours.base_04.into(),
Base16Value::Base05 => colours.base_05.into(),
Base16Value::Base06 => colours.base_06.into(),
Base16Value::Base07 => colours.base_07.into(),
Base16Value::Base08 => colours.base_08.into(),
Base16Value::Base09 => colours.base_09.into(),
Base16Value::Base0A => colours.base_0a.into(),
Base16Value::Base0B => colours.base_0b.into(),
Base16Value::Base0C => colours.base_0c.into(),
Base16Value::Base0D => colours.base_0d.into(),
Base16Value::Base0E => colours.base_0e.into(),
Base16Value::Base0F => colours.base_0f.into(),
},
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "komorebi"
version = "0.1.35"
version = "0.1.36"
description = "A tiling window manager for Windows"
repository = "https://github.com/LGUG2Z/komorebi"
edition = "2021"
@@ -19,7 +19,6 @@ ctrlc = { version = "3", features = ["termination"] }
dirs = { workspace = true }
dunce = { workspace = true }
getset = "0.1"
hex_color = { version = "3", features = ["serde"] }
hotwatch = { workspace = true }
lazy_static = { workspace = true }
miow = "0.6"
@@ -28,8 +27,9 @@ net2 = "0.2"
os_info = "3.10"
parking_lot = "0.12"
paste = { workspace = true }
powershell_script = "1.0"
regex = "1"
schemars = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
@@ -49,12 +49,16 @@ windows-implement = { workspace = true }
windows-interface = { workspace = true }
winput = "0.2"
winreg = "0.55"
serde_with = { version = "3.12", features = ["schemars_0_8"] }
[build-dependencies]
shadow-rs = { workspace = true }
[dev-dependencies]
reqwest = { version = "0.12", features = ["blocking"] }
uuid = { version = "1", features = ["v4"] }
[features]
default = ["schemars"]
deadlock_detection = ["parking_lot/deadlock_detection"]
schemars = ["dep:schemars"]

View File

@@ -1,7 +1,5 @@
use color_eyre::Result;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::sync::atomic::Ordering;
@@ -13,7 +11,8 @@ use super::ANIMATION_DURATION_GLOBAL;
use super::ANIMATION_FPS;
use super::ANIMATION_MANAGER;
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct AnimationEngine;
impl AnimationEngine {

View File

@@ -18,11 +18,12 @@ pub mod prefix;
pub mod render_dispatcher;
pub use render_dispatcher::RenderDispatcher;
pub mod style;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum PerAnimationPrefixConfig<T> {
Prefix(HashMap<AnimationPrefix, T>),

View File

@@ -1,24 +1,14 @@
use clap::ValueEnum;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;
#[derive(
Copy,
Clone,
Debug,
Hash,
PartialEq,
Eq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize, Display, EnumString, ValueEnum,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum AnimationPrefix {

View File

@@ -3,21 +3,17 @@ use crate::border_manager::RenderTarget;
use crate::border_manager::WindowKind;
use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::border_manager::FOCUS_STATE;
use crate::border_manager::RENDER_TARGETS;
use crate::border_manager::STYLE;
use crate::core::BorderStyle;
use crate::core::Rect;
use crate::windows_api;
use crate::WindowsApi;
use crate::WINDOWS_11;
use color_eyre::eyre::anyhow;
use std::collections::HashMap;
use std::ops::Deref;
use std::sync::atomic::Ordering;
use std::sync::mpsc;
use std::sync::LazyLock;
use std::sync::OnceLock;
use windows::Win32::Foundation::FALSE;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::LPARAM;
@@ -53,18 +49,22 @@ use windows::Win32::UI::WindowsAndMessaging::DispatchMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetMessageW;
use windows::Win32::UI::WindowsAndMessaging::GetSystemMetrics;
use windows::Win32::UI::WindowsAndMessaging::GetWindowLongPtrW;
use windows::Win32::UI::WindowsAndMessaging::LoadCursorW;
use windows::Win32::UI::WindowsAndMessaging::PostQuitMessage;
use windows::Win32::UI::WindowsAndMessaging::SetCursor;
use windows::Win32::UI::WindowsAndMessaging::SetWindowLongPtrW;
use windows::Win32::UI::WindowsAndMessaging::TranslateMessage;
use windows::Win32::UI::WindowsAndMessaging::CREATESTRUCTW;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::EVENT_OBJECT_LOCATIONCHANGE;
use windows::Win32::UI::WindowsAndMessaging::GWLP_USERDATA;
use windows::Win32::UI::WindowsAndMessaging::IDC_ARROW;
use windows::Win32::UI::WindowsAndMessaging::MSG;
use windows::Win32::UI::WindowsAndMessaging::SM_CXVIRTUALSCREEN;
use windows::Win32::UI::WindowsAndMessaging::WM_CREATE;
use windows::Win32::UI::WindowsAndMessaging::WM_DESTROY;
use windows::Win32::UI::WindowsAndMessaging::WM_PAINT;
use windows::Win32::UI::WindowsAndMessaging::WM_SETCURSOR;
use windows::Win32::UI::WindowsAndMessaging::WNDCLASSW;
use windows_core::BOOL;
use windows_core::PCWSTR;
@@ -114,7 +114,9 @@ pub extern "system" fn border_hwnds(hwnd: HWND, lparam: LPARAM) -> BOOL {
#[derive(Debug, Clone)]
pub struct Border {
pub hwnd: isize,
pub render_target: OnceLock<RenderTarget>,
pub id: String,
pub monitor_idx: Option<usize>,
pub render_target: Option<RenderTarget>,
pub tracking_hwnd: isize,
pub window_rect: Rect,
pub window_kind: WindowKind,
@@ -130,7 +132,9 @@ impl From<isize> for Border {
fn from(value: isize) -> Self {
Self {
hwnd: value,
render_target: OnceLock::new(),
id: String::new(),
monitor_idx: None,
render_target: None,
tracking_hwnd: 0,
window_rect: Rect::default(),
window_kind: WindowKind::Unfocused,
@@ -149,7 +153,11 @@ impl Border {
HWND(windows_api::as_ptr!(self.hwnd))
}
pub fn create(id: &str, tracking_hwnd: isize) -> color_eyre::Result<Self> {
pub fn create(
id: &str,
tracking_hwnd: isize,
monitor_idx: usize,
) -> color_eyre::Result<Box<Self>> {
let name: Vec<u16> = format!("komoborder-{id}\0").encode_utf16().collect();
let class_name = PCWSTR(name.as_ptr());
@@ -168,10 +176,13 @@ impl Border {
let (border_sender, border_receiver) = mpsc::channel();
let instance = h_module.0 as isize;
let container_id = id.to_owned();
std::thread::spawn(move || -> color_eyre::Result<()> {
let mut border = Self {
hwnd: 0,
render_target: OnceLock::new(),
id: container_id,
monitor_idx: Some(monitor_idx),
render_target: None,
tracking_hwnd,
window_rect: WindowsApi::window_rect(tracking_hwnd).unwrap_or_default(),
window_kind: WindowKind::Unfocused,
@@ -183,12 +194,15 @@ impl Border {
brushes: HashMap::new(),
};
let border_pointer = std::ptr::addr_of!(border);
let border_pointer = &raw mut border;
let hwnd =
WindowsApi::create_border_window(PCWSTR(name.as_ptr()), instance, border_pointer)?;
border.hwnd = hwnd;
border_sender.send(border_pointer as isize)?;
let boxed = unsafe {
(*border_pointer).hwnd = hwnd;
Box::from_raw(border_pointer)
};
border_sender.send(boxed)?;
let mut msg: MSG = MSG::default();
@@ -207,8 +221,7 @@ impl Border {
Ok(())
});
let border_ref = border_receiver.recv()?;
let border = unsafe { &mut *(border_ref as *mut Border) };
let mut border = border_receiver.recv()?;
// I have literally no idea, apparently this is to get rid of the black pixels
// around the edges of rounded corners? @lukeyou05 borrowed this from PowerToys
@@ -228,8 +241,14 @@ impl Border {
let _ = DwmEnableBlurBehindWindow(border.hwnd(), &bh);
}
border.update_brushes()?;
Ok(border)
}
pub fn update_brushes(&mut self) -> color_eyre::Result<()> {
let hwnd_render_target_properties = D2D1_HWND_RENDER_TARGET_PROPERTIES {
hwnd: HWND(windows_api::as_ptr!(border.hwnd)),
hwnd: HWND(windows_api::as_ptr!(self.hwnd)),
pixelSize: Default::default(),
presentOptions: D2D1_PRESENT_OPTIONS_IMMEDIATELY,
};
@@ -250,13 +269,14 @@ impl Border {
.CreateHwndRenderTarget(&render_target_properties, &hwnd_render_target_properties)
} {
Ok(render_target) => unsafe {
border.brush_properties = *BRUSH_PROPERTIES.deref();
self.brush_properties = *BRUSH_PROPERTIES.deref();
for window_kind in [
WindowKind::Single,
WindowKind::Stack,
WindowKind::Monocle,
WindowKind::Unfocused,
WindowKind::Floating,
WindowKind::UnfocusedLocked,
] {
let color = window_kind_colour(window_kind);
let color = D2D1_COLOR_F {
@@ -267,24 +287,18 @@ impl Border {
};
if let Ok(brush) =
render_target.CreateSolidColorBrush(&color, Some(&border.brush_properties))
render_target.CreateSolidColorBrush(&color, Some(&self.brush_properties))
{
border.brushes.insert(window_kind, brush);
self.brushes.insert(window_kind, brush);
}
}
render_target.SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
if border
.render_target
.set(RenderTarget(render_target.clone()))
.is_err()
{
return Err(anyhow!("could not store border render target"));
}
self.render_target = Some(RenderTarget(render_target));
border.rounded_rect = {
let radius = 8.0 + border.width as f32 / 2.0;
self.rounded_rect = {
let radius = 8.0 + self.width as f32 / 2.0;
D2D1_ROUNDED_RECT {
rect: Default::default(),
radiusX: radius,
@@ -292,17 +306,13 @@ impl Border {
}
};
let mut render_targets = RENDER_TARGETS.lock();
render_targets.insert(border.hwnd, RenderTarget(render_target));
Ok(border.clone())
Ok(())
},
Err(error) => Err(error.into()),
}
}
pub fn destroy(&self) -> color_eyre::Result<()> {
let mut render_targets = RENDER_TARGETS.lock();
render_targets.remove(&self.hwnd);
WindowsApi::close_window(self.hwnd)
}
@@ -329,6 +339,16 @@ impl Border {
) -> LRESULT {
unsafe {
match message {
WM_SETCURSOR => match LoadCursorW(None, IDC_ARROW) {
Ok(cursor) => {
SetCursor(Some(cursor));
LRESULT(0)
}
Err(error) => {
tracing::error!("{error}");
LRESULT(1)
}
},
WM_CREATE => {
let mut border_pointer: *mut Border =
GetWindowLongPtrW(window, GWLP_USERDATA) as _;
@@ -361,7 +381,7 @@ impl Border {
return LRESULT(0);
}
let reference_hwnd = lparam.0;
let reference_hwnd = (*border_pointer).tracking_hwnd;
let old_rect = (*border_pointer).window_rect;
let rect = WindowsApi::window_rect(reference_hwnd).unwrap_or_default();
@@ -373,7 +393,7 @@ impl Border {
}
if !rect.is_same_size_as(&old_rect) {
if let Some(render_target) = (*border_pointer).render_target.get() {
if let Some(render_target) = (*border_pointer).render_target.as_ref() {
let border_width = (*border_pointer).width;
let border_offset = (*border_pointer).offset;
@@ -455,7 +475,7 @@ impl Border {
tracing::error!("failed to update border position {error}");
}
if let Some(render_target) = (*border_pointer).render_target.get() {
if let Some(render_target) = (*border_pointer).render_target.as_ref() {
(*border_pointer).width = BORDER_WIDTH.load(Ordering::Relaxed);
(*border_pointer).offset = BORDER_OFFSET.load(Ordering::Relaxed);
@@ -475,11 +495,6 @@ impl Border {
});
// Get window kind and color
(*border_pointer).window_kind = FOCUS_STATE
.lock()
.get(&(window.0 as isize))
.copied()
.unwrap_or(WindowKind::Unfocused);
let window_kind = (*border_pointer).window_kind;
if let Some(brush) = (*border_pointer).brushes.get(&window_kind) {
render_target.BeginDraw();

View File

@@ -5,10 +5,8 @@ use crate::core::BorderImplementation;
use crate::core::BorderStyle;
use crate::core::WindowKind;
use crate::ring::Ring;
use crate::windows_api;
use crate::workspace::WorkspaceLayer;
use crate::workspace_reconciliator::ALT_TAB_HWND;
use crate::Colour;
use crate::Rgb;
use crate::WindowManager;
use crate::WindowsApi;
use border::border_hwnds;
@@ -17,9 +15,10 @@ use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicCell;
use crossbeam_utils::atomic::AtomicConsume;
use komorebi_themes::colour::Colour;
use komorebi_themes::colour::Rgb;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::hash_map::Entry;
@@ -32,6 +31,7 @@ use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::OnceLock;
use strum::Display;
use windows::Win32::Foundation::HWND;
use windows::Win32::Graphics::Direct2D::ID2D1HwndRenderTarget;
pub static BORDER_WIDTH: AtomicI32 = AtomicI32::new(8);
@@ -47,6 +47,8 @@ lazy_static! {
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(66, 165, 245))));
pub static ref UNFOCUSED: AtomicU32 =
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(128, 128, 128))));
pub static ref UNFOCUSED_LOCKED: AtomicU32 =
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(158, 8, 8))));
pub static ref MONOCLE: AtomicU32 =
AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(255, 51, 153))));
pub static ref STACK: AtomicU32 = AtomicU32::new(u32::from(Colour::Rgb(Rgb::new(0, 165, 66))));
@@ -55,11 +57,8 @@ lazy_static! {
}
lazy_static! {
static ref BORDERS_MONITORS: Mutex<HashMap<String, usize>> = Mutex::new(HashMap::new());
static ref BORDER_STATE: Mutex<HashMap<String, Border>> = Mutex::new(HashMap::new());
static ref WINDOWS_BORDERS: Mutex<HashMap<isize, Border>> = Mutex::new(HashMap::new());
static ref FOCUS_STATE: Mutex<HashMap<isize, WindowKind>> = Mutex::new(HashMap::new());
static ref RENDER_TARGETS: Mutex<HashMap<isize, RenderTarget>> = Mutex::new(HashMap::new());
static ref BORDER_STATE: Mutex<HashMap<String, Box<Border>>> = Mutex::new(HashMap::new());
static ref WINDOWS_BORDERS: Mutex<HashMap<isize, String>> = Mutex::new(HashMap::new());
}
#[derive(Debug, Clone)]
@@ -74,7 +73,22 @@ impl Deref for RenderTarget {
}
}
pub struct Notification(pub Option<isize>);
pub enum Notification {
Update(Option<isize>),
ForceUpdate,
}
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct BorderInfo {
pub border_hwnd: isize,
pub window_kind: WindowKind,
}
impl BorderInfo {
pub fn hwnd(&self) -> HWND {
HWND(windows_api::as_ptr!(self.border_hwnd))
}
}
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
@@ -90,12 +104,22 @@ fn event_rx() -> Receiver<Notification> {
channel().1.clone()
}
pub fn window_border(hwnd: isize) -> Option<Border> {
WINDOWS_BORDERS.lock().get(&hwnd).cloned()
pub fn window_border(hwnd: isize) -> Option<BorderInfo> {
let id = WINDOWS_BORDERS.lock().get(&hwnd)?.clone();
BORDER_STATE.lock().get(&id).map(|b| BorderInfo {
border_hwnd: b.hwnd,
window_kind: b.window_kind,
})
}
pub fn send_notification(hwnd: Option<isize>) {
if event_tx().try_send(Notification(hwnd)).is_err() {
if event_tx().try_send(Notification::Update(hwnd)).is_err() {
tracing::warn!("channel is full; dropping notification")
}
}
pub fn send_force_update() {
if event_tx().try_send(Notification::ForceUpdate).is_err() {
tracing::warn!("channel is full; dropping notification")
}
}
@@ -107,15 +131,13 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
borders.iter().map(|b| b.1.hwnd).collect::<Vec<_>>()
);
for (_, border) in borders.iter() {
let _ = border.destroy();
for (_, border) in borders.drain() {
let _ = destroy_border(border);
}
borders.clear();
BORDERS_MONITORS.lock().clear();
drop(borders);
WINDOWS_BORDERS.lock().clear();
FOCUS_STATE.lock().clear();
RENDER_TARGETS.lock().clear();
let mut remaining_hwnds = vec![];
@@ -128,7 +150,7 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
tracing::info!("purging unknown borders: {:?}", remaining_hwnds);
for hwnd in remaining_hwnds {
let _ = Border::from(hwnd).destroy();
let _ = destroy_border(Box::new(Border::from(hwnd)));
}
}
@@ -138,6 +160,7 @@ pub fn destroy_all_borders() -> color_eyre::Result<()> {
fn window_kind_colour(focus_kind: WindowKind) -> u32 {
match focus_kind {
WindowKind::Unfocused => UNFOCUSED.load(Ordering::Relaxed),
WindowKind::UnfocusedLocked => UNFOCUSED_LOCKED.load(Ordering::Relaxed),
WindowKind::Single => FOCUSED.load(Ordering::Relaxed),
WindowKind::Stack => STACK.load(Ordering::Relaxed),
WindowKind::Monocle => MONOCLE.load(Ordering::Relaxed),
@@ -162,7 +185,7 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
tracing::info!("listening");
let receiver = event_rx();
event_tx().send(Notification(None))?;
event_tx().send(Notification::Update(None))?;
let mut previous_snapshot = Ring::default();
let mut previous_pending_move_op = None;
@@ -218,7 +241,11 @@ 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
{
WindowKind::Unfocused
if ws.locked_containers().contains(&idx) {
WindowKind::UnfocusedLocked
} else {
WindowKind::Unfocused
}
} else if c.windows().len() > 1 {
WindowKind::Stack
} else {
@@ -244,54 +271,75 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
}
BorderImplementation::Komorebi => {
let mut should_process_notification = true;
let should_process_notification = match notification {
Notification::Update(notification_hwnd) => {
let mut should_process_notification = true;
if monitors == previous_snapshot
// handle the window dragging edge case
&& pending_move_op == previous_pending_move_op
{
should_process_notification = false;
}
if monitors == previous_snapshot
// handle the window dragging edge case
&& pending_move_op == previous_pending_move_op
{
should_process_notification = false;
}
// handle the pause edge case
if is_paused && !previous_is_paused {
should_process_notification = true;
}
// handle the unpause edge case
if previous_is_paused && !is_paused {
should_process_notification = true;
}
// handle the retile edge case
if !should_process_notification && BORDER_STATE.lock().is_empty() {
should_process_notification = true;
}
// when we switch focus to/from a floating window
let switch_focus_to_from_floating_window = floating_window_hwnds.iter().any(|fw| {
// if we switch focus to a floating window
fw == &notification.0.unwrap_or_default() ||
// if there is any floating window with a `WindowKind::Floating` border
// that no longer is the foreground window then we need to update that
// border.
(fw != &foreground_window
&& window_border(*fw)
.map(|b| b.window_kind == WindowKind::Floating)
.unwrap_or_default())
});
if !should_process_notification && switch_focus_to_from_floating_window {
should_process_notification = true;
}
if !should_process_notification {
if let Some(ref previous) = previous_notification {
if previous.0.unwrap_or_default() != notification.0.unwrap_or_default() {
// handle the pause edge case
if is_paused && !previous_is_paused {
should_process_notification = true;
}
// handle the unpause edge case
if previous_is_paused && !is_paused {
should_process_notification = true;
}
// handle the retile edge case
if !should_process_notification && BORDER_STATE.lock().is_empty() {
should_process_notification = true;
}
// when we switch focus to/from a floating window
let switch_focus_to_from_floating_window =
floating_window_hwnds.iter().any(|fw| {
// if we switch focus to a floating window
fw == &notification_hwnd.unwrap_or_default() ||
// if there is any floating window with a `WindowKind::Floating` border
// that no longer is the foreground window then we need to update that
// border.
(fw != &foreground_window
&& window_border(*fw)
.is_some_and(|b| b.window_kind == WindowKind::Floating))
});
// when the focused window has an `Unfocused` border kind, usually this happens if
// we focus an admin window and then refocus the previously focused window. For
// komorebi it will have the same state has before, however the previously focused
// window changed its border to unfocused so now we need to update it again.
if !should_process_notification
&& window_border(notification_hwnd.unwrap_or_default())
.is_some_and(|b| b.window_kind == WindowKind::Unfocused)
{
should_process_notification = true;
}
if !should_process_notification && switch_focus_to_from_floating_window {
should_process_notification = true;
}
if !should_process_notification {
if let Some(Notification::Update(ref previous)) = previous_notification
{
if previous.unwrap_or_default()
!= notification_hwnd.unwrap_or_default()
{
should_process_notification = true;
}
}
}
should_process_notification
}
}
Notification::ForceUpdate => true,
};
if !should_process_notification {
tracing::trace!("monitor state matches latest snapshot, skipping notification");
@@ -299,26 +347,19 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
let mut borders = BORDER_STATE.lock();
let mut borders_monitors = BORDERS_MONITORS.lock();
let mut windows_borders = WINDOWS_BORDERS.lock();
let mut focus_state = FOCUS_STATE.lock();
// If borders are disabled
if !BORDER_ENABLED.load_consume()
// Or if the wm is paused
|| is_paused
// Or if we are handling an alt-tab across workspaces
|| ALT_TAB_HWND.load().is_some()
{
// Destroy the borders we know about
for (_, border) in borders.iter() {
border.destroy()?;
for (_, border) in borders.drain() {
destroy_border(border)?;
}
borders.clear();
borders_monitors.clear();
windows_borders.clear();
focus_state.clear();
previous_is_paused = is_paused;
continue 'receiver;
@@ -333,8 +374,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
remove_borders(
&mut borders,
&mut windows_borders,
&mut focus_state,
&mut borders_monitors,
monitor_idx,
|_, _| true,
)?;
@@ -345,12 +384,16 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
// Handle the monocle container separately
if let Some(monocle) = ws.monocle_container() {
let mut new_border = false;
let border = match borders.entry(monocle.id().clone()) {
let focused_window_hwnd =
monocle.focused_window().map(|w| w.hwnd).unwrap_or_default();
let id = monocle.id().clone();
let border = match borders.entry(id.clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(
monocle.id(),
monocle.focused_window().copied().unwrap_or_default().hwnd,
focused_window_hwnd,
monitor_idx,
) {
new_border = true;
entry.insert(border)
@@ -366,32 +409,49 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
WindowKind::Monocle
};
border.window_kind = new_focus_state;
focus_state.insert(border.hwnd, new_focus_state);
let reference_hwnd =
monocle.focused_window().copied().unwrap_or_default().hwnd;
// Update the borders tracking_hwnd in case it changed and remove the
// old `tracking_hwnd` from `WINDOWS_BORDERS` if needed.
if border.tracking_hwnd != focused_window_hwnd {
if let Some(previous) = windows_borders.get(&border.tracking_hwnd) {
// Only remove the border from `windows_borders` if it
// still corresponds to the same border, if doesn't then
// it means it was already updated by another border for
// that window and in that case we don't want to remove it.
if previous == &id {
windows_borders.remove(&border.tracking_hwnd);
}
}
border.tracking_hwnd = focused_window_hwnd;
if !WindowsApi::is_window_visible(border.hwnd) {
WindowsApi::restore_window(border.hwnd);
}
}
let rect = WindowsApi::window_rect(reference_hwnd)?;
// Update the border's monitor idx in case it changed
border.monitor_idx = Some(monitor_idx);
let rect = WindowsApi::window_rect(focused_window_hwnd)?;
border.window_rect = rect;
if new_border {
border.set_position(&rect, reference_hwnd)?;
border.set_position(&rect, focused_window_hwnd)?;
} else if matches!(notification, Notification::ForceUpdate) {
// Update the border brushes if there was a forced update
// notification and this is not a new border (new border's
// already have their brushes updated on creation)
border.update_brushes()?;
}
border.invalidate();
borders_monitors.insert(monocle.id().clone(), monitor_idx);
windows_borders.insert(
monocle.focused_window().cloned().unwrap_or_default().hwnd,
border.clone(),
);
windows_borders.insert(focused_window_hwnd, id);
let border_hwnd = border.hwnd;
// Remove all borders on this monitor except monocle
remove_borders(
&mut borders,
&mut windows_borders,
&mut focus_state,
&mut borders_monitors,
monitor_idx,
|_, b| border_hwnd != b.hwnd,
)?;
@@ -410,8 +470,6 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
remove_borders(
&mut borders,
&mut windows_borders,
&mut focus_state,
&mut borders_monitors,
monitor_idx,
|_, _| true,
)?;
@@ -434,56 +492,22 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
remove_borders(
&mut borders,
&mut windows_borders,
&mut focus_state,
&mut borders_monitors,
monitor_idx,
|id, _| !container_and_floating_window_ids.contains(id),
)?;
'containers: for (idx, c) in ws.containers().iter().enumerate() {
// In case this container is a stack we need to check it's
// unfocused windows to remove any attached border
let is_stack = c.windows().len() > 1;
if is_stack {
let focused_window_idx = c.focused_window_idx();
let potential_stacked_border_handles = c
.windows()
.iter()
.enumerate()
.flat_map(|(i, w)| {
if i != focused_window_idx {
windows_borders.get(&w.hwnd).map(|b| b.hwnd)
} else {
None
}
})
.collect::<Vec<_>>();
if !potential_stacked_border_handles.is_empty() {
tracing::debug!(
"purging stacked borders: {:?}",
potential_stacked_border_handles
);
remove_borders(
&mut borders,
&mut windows_borders,
&mut focus_state,
&mut borders_monitors,
monitor_idx,
|_, b| potential_stacked_border_handles.contains(&b.hwnd),
)?;
}
}
let focused_window_hwnd =
c.focused_window().map(|w| w.hwnd).unwrap_or_default();
let id = c.id().clone();
// Get the border entry for this container from the map or create one
let mut new_border = false;
let border = match borders.entry(c.id().clone()) {
let border = match borders.entry(id.clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) = Border::create(c.id(), focused_window_hwnd)
if let Ok(border) =
Border::create(c.id(), focused_window_hwnd, monitor_idx)
{
new_border = true;
entry.insert(border)
@@ -493,107 +517,92 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
};
#[allow(unused_assignments)]
let mut last_focus_state = None;
let last_focus_state = border.window_kind;
let new_focus_state = if idx != ws.focused_container_idx()
|| monitor_idx != focused_monitor_idx
|| focused_window_hwnd != foreground_window
{
WindowKind::Unfocused
if ws.locked_containers().contains(&idx) {
WindowKind::UnfocusedLocked
} else {
WindowKind::Unfocused
}
} else if c.windows().len() > 1 {
WindowKind::Stack
} else {
WindowKind::Single
};
border.window_kind = new_focus_state;
last_focus_state = focus_state.get(&border.hwnd).copied();
// If this container's border was previously tracking a different
// window, then we need to destroy that border and create a new one
// tracking the correct window.
// Update the borders `tracking_hwnd` in case it changed and remove the
// old `tracking_hwnd` from `WINDOWS_BORDERS` if needed.
if border.tracking_hwnd != focused_window_hwnd {
// Create new border
if let Ok(b) = Border::create(
c.id(),
c.focused_window().copied().unwrap_or_default().hwnd,
) {
// Destroy previously stacked border window and remove its hwnd
// and tracking_hwnd.
border.destroy()?;
focus_state.remove(&border.hwnd);
if let Some(previous) =
windows_borders.get(&border.tracking_hwnd)
{
// Only remove the border from `windows_borders` if it
// still is the same border, if it isn't then it means it
// was already updated by another border for that window
// and in that case we don't want to remove it.
if previous.hwnd == border.hwnd {
windows_borders.remove(&border.tracking_hwnd);
}
if let Some(previous) = windows_borders.get(&border.tracking_hwnd) {
// Only remove the border from `windows_borders` if it
// still corresponds to the same border, if doesn't then
// it means it was already updated by another border for
// that window and in that case we don't want to remove it.
if previous == &id {
windows_borders.remove(&border.tracking_hwnd);
}
// Replace with new border
new_border = true;
*border = b;
} else {
continue 'monitors;
}
border.tracking_hwnd = focused_window_hwnd;
if !WindowsApi::is_window_visible(border.hwnd) {
WindowsApi::restore_window(border.hwnd);
}
}
// Update the border's monitor idx in case it changed
border.monitor_idx = Some(monitor_idx);
// avoid getting into a thread restart loop if we try to look up
// rect info for a window that has been destroyed by the time
// we get here
let rect = match WindowsApi::window_rect(focused_window_hwnd) {
Ok(rect) => rect,
Err(_) => {
remove_border(
c.id(),
&mut borders,
&mut windows_borders,
&mut focus_state,
&mut borders_monitors,
)?;
remove_border(c.id(), &mut borders, &mut windows_borders)?;
continue 'containers;
}
};
border.window_rect = rect;
let layer_changed = previous_layer != workspace_layer;
let forced_update = matches!(notification, Notification::ForceUpdate);
let should_invalidate = match last_focus_state {
None => true,
Some(last_focus_state) => {
(last_focus_state != new_focus_state) || layer_changed
}
};
if new_border || should_invalidate {
border.set_position(&rect, focused_window_hwnd)?;
}
let should_invalidate = new_border
|| (last_focus_state != new_focus_state)
|| layer_changed
|| forced_update;
if should_invalidate {
if forced_update && !new_border {
// Update the border brushes if there was a forced update
// notification and this is not a new border (new border's
// already have their brushes updated on creation)
border.update_brushes()?;
}
border.set_position(&rect, focused_window_hwnd)?;
border.invalidate();
}
borders_monitors.insert(c.id().clone(), monitor_idx);
windows_borders.insert(
c.focused_window().cloned().unwrap_or_default().hwnd,
border.clone(),
);
focus_state.insert(border.hwnd, new_focus_state);
windows_borders.insert(focused_window_hwnd, id);
}
{
for window in ws.floating_windows() {
let mut new_border = false;
let border = match borders.entry(window.hwnd.to_string()) {
let id = window.hwnd.to_string();
let border = match borders.entry(id.clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
if let Ok(border) =
Border::create(&window.hwnd.to_string(), window.hwnd)
{
if let Ok(border) = Border::create(
&window.hwnd.to_string(),
window.hwnd,
monitor_idx,
) {
new_border = true;
entry.insert(border)
} else {
@@ -602,39 +611,43 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
}
};
#[allow(unused_assignments)]
let mut last_focus_state = None;
let mut new_focus_state = WindowKind::Unfocused;
let last_focus_state = border.window_kind;
if foreground_window == window.hwnd {
new_focus_state = WindowKind::Floating;
}
border.window_kind = new_focus_state;
last_focus_state = focus_state.get(&border.hwnd).copied();
let rect = WindowsApi::window_rect(window.hwnd)?;
let layer_changed = previous_layer != workspace_layer;
let should_invalidate = match last_focus_state {
None => true,
Some(last_focus_state) => {
last_focus_state != new_focus_state || layer_changed
}
let new_focus_state = if foreground_window == window.hwnd {
WindowKind::Floating
} else {
WindowKind::Unfocused
};
if new_border {
border.set_position(&rect, window.hwnd)?;
}
border.window_kind = new_focus_state;
// Update the border's monitor idx in case it changed
border.monitor_idx = Some(monitor_idx);
let rect = WindowsApi::window_rect(window.hwnd)?;
border.window_rect = rect;
let layer_changed = previous_layer != workspace_layer;
let forced_update =
matches!(notification, Notification::ForceUpdate);
let should_invalidate = new_border
|| (last_focus_state != new_focus_state)
|| layer_changed
|| forced_update;
if should_invalidate {
if forced_update && !new_border {
// Update the border brushes if there was a forced update
// notification and this is not a new border (new border's
// already have their brushes updated on creation)
border.update_brushes()?;
}
border.set_position(&rect, window.hwnd)?;
border.invalidate();
}
borders_monitors.insert(window.hwnd.to_string(), monitor_idx);
windows_borders.insert(window.hwnd, border.clone());
focus_state.insert(border.hwnd, new_focus_state);
windows_borders.insert(window.hwnd, id);
}
}
}
@@ -657,24 +670,27 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
/// the container id and the border and returns a bool, if true that border
/// will be removed.
fn remove_borders(
borders: &mut HashMap<String, Border>,
windows_borders: &mut HashMap<isize, Border>,
focus_state: &mut HashMap<isize, WindowKind>,
borders_monitors: &mut HashMap<String, usize>,
borders: &mut HashMap<String, Box<Border>>,
windows_borders: &mut HashMap<isize, String>,
monitor_idx: usize,
condition: impl Fn(&String, &Border) -> bool,
) -> color_eyre::Result<()> {
let mut to_remove = vec![];
for (id, border) in borders.iter() {
if borders_monitors.get(id).copied().unwrap_or_default() == monitor_idx
// if border is on this monitor
if border.monitor_idx.is_some_and(|idx| idx == monitor_idx)
// and the condition applies
&& condition(id, border)
// and the border is visible (we don't remove hidden borders)
&& WindowsApi::is_window_visible(border.hwnd)
{
// we mark it to be removed
to_remove.push(id.clone());
}
}
for id in &to_remove {
remove_border(id, borders, windows_borders, focus_state, borders_monitors)?;
remove_border(id, borders, windows_borders)?;
}
Ok(())
@@ -683,22 +699,73 @@ fn remove_borders(
/// Removes the border with `id` and all its related info from all maps
fn remove_border(
id: &str,
borders: &mut HashMap<String, Border>,
windows_borders: &mut HashMap<isize, Border>,
focus_state: &mut HashMap<isize, WindowKind>,
borders_monitors: &mut HashMap<String, usize>,
borders: &mut HashMap<String, Box<Border>>,
windows_borders: &mut HashMap<isize, String>,
) -> color_eyre::Result<()> {
if let Some(removed_border) = borders.remove(id) {
removed_border.destroy()?;
windows_borders.remove(&removed_border.tracking_hwnd);
focus_state.remove(&removed_border.hwnd);
destroy_border(removed_border)?;
}
borders_monitors.remove(id);
Ok(())
}
#[derive(Debug, Copy, Clone, Display, Serialize, Deserialize, JsonSchema, PartialEq)]
/// IMPORTANT: BEWARE when changing this function. We need to make sure that we don't let the
/// `Box<Border>` be dropped normally. We need to turn the `Box` into the raw pointer and use that
/// pointer to call the `.destroy()` funtion of the border so it closes the window. This way the
/// `Box` is consumed and the pointer is dropped like a normal `Copy` number instead of trying to
/// drop the struct it points to. The actual border is owned by the thread that created the window
/// and once the window closes that thread gets out of its loop, finishes and properly disposes of
/// the border.
fn destroy_border(border: Box<Border>) -> color_eyre::Result<()> {
let raw_pointer = Box::into_raw(border);
unsafe {
(*raw_pointer).destroy()?;
}
Ok(())
}
/// Removes the border around window with `tracking_hwnd` if it exists
pub fn delete_border(tracking_hwnd: isize) {
std::thread::spawn(move || {
let id = {
WINDOWS_BORDERS
.lock()
.get(&tracking_hwnd)
.cloned()
.unwrap_or_default()
};
let mut borders = BORDER_STATE.lock();
let mut windows_borders = WINDOWS_BORDERS.lock();
if let Err(error) = remove_border(&id, &mut borders, &mut windows_borders) {
tracing::error!("Failed to delete border: {}", error);
}
});
}
/// Shows the border around window with `tracking_hwnd` if it exists
pub fn show_border(tracking_hwnd: isize) {
std::thread::spawn(move || {
if let Some(border_info) = window_border(tracking_hwnd) {
WindowsApi::restore_window(border_info.border_hwnd);
}
});
}
/// Hides the border around window with `tracking_hwnd` if it exists, unless the border kind is a
/// `Stack` border.
pub fn hide_border(tracking_hwnd: isize) {
std::thread::spawn(move || {
if let Some(border_info) = window_border(tracking_hwnd) {
WindowsApi::hide_window(border_info.border_hwnd);
}
});
}
#[derive(Debug, Copy, Clone, Display, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum ZOrder {
Top,
NoTopMost,

View File

@@ -2,14 +2,14 @@ use std::collections::VecDeque;
use getset::Getters;
use nanoid::nanoid;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use crate::ring::Ring;
use crate::window::Window;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters, JsonSchema)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Getters)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Container {
#[getset(get = "pub")]
id: String,
@@ -52,13 +52,17 @@ impl Container {
}
}
/// Hides the unfocused windows of the container and restores the focused one. This function
/// is used to make sure we update the window that should be shown on a stack. If the container
/// isn't a stack this function won't change anything.
pub fn load_focused_window(&mut self) {
let focused_idx = self.focused_window_idx();
for (i, window) in self.windows_mut().iter_mut().enumerate() {
if i == focused_idx {
window.restore();
window.restore_with_border(false);
} else {
window.hide();
window.hide_with_border(false);
}
}
}
@@ -98,14 +102,13 @@ impl Container {
}
pub fn idx_for_window(&self, hwnd: isize) -> Option<usize> {
let mut idx = None;
for (i, window) in self.windows().iter().enumerate() {
if window.hwnd == hwnd {
idx = Option::from(i);
return Option::from(i);
}
}
idx
None
}
pub fn remove_window_by_idx(&mut self, idx: usize) -> Option<Window> {
@@ -137,3 +140,114 @@ impl Container {
self.windows.focus(idx);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_contains_window() {
let mut container = Container::default();
for i in 0..3 {
container.add_window(Window::from(i));
}
// Should return true for existing windows
assert!(container.contains_window(1));
assert_eq!(container.idx_for_window(1), Some(1));
// Should return false since window 4 doesn't exist
assert!(!container.contains_window(4));
assert_eq!(container.idx_for_window(4), None);
}
#[test]
fn test_remove_window_by_idx() {
let mut container = Container::default();
for i in 0..3 {
container.add_window(Window::from(i));
}
// Remove window 1
container.remove_window_by_idx(1);
// Should only have 2 windows left
assert_eq!(container.windows().len(), 2);
// Should return false since window 1 was removed
assert!(!container.contains_window(1));
}
#[test]
fn test_remove_focused_window() {
let mut container = Container::default();
for i in 0..3 {
container.add_window(Window::from(i));
}
// Should be focused on the last created window
assert_eq!(container.focused_window_idx(), 2);
// Remove the focused window
container.remove_focused_window();
// Should be focused on the window before the removed one
assert_eq!(container.focused_window_idx(), 1);
// Should only have 2 windows left
assert_eq!(container.windows().len(), 2);
}
#[test]
fn test_add_window() {
let mut container = Container::default();
container.add_window(Window::from(1));
assert_eq!(container.windows().len(), 1);
assert_eq!(container.focused_window_idx(), 0);
assert!(container.contains_window(1));
}
#[test]
fn test_focus_window() {
let mut container = Container::default();
for i in 0..3 {
container.add_window(Window::from(i));
}
// Should focus on the last created window
assert_eq!(container.focused_window_idx(), 2);
// focus on the window at index 1
container.focus_window(1);
// Should be focused on window 1
assert_eq!(container.focused_window_idx(), 1);
// focus on the window at index 0
container.focus_window(0);
// Should be focused on window 0
assert_eq!(container.focused_window_idx(), 0);
}
#[test]
fn test_idx_for_window() {
let mut container = Container::default();
for i in 0..3 {
container.add_window(Window::from(i));
}
// Should return the index of the window
assert_eq!(container.idx_for_window(1), Some(1));
// Should return None since window 4 doesn't exist
assert_eq!(container.idx_for_window(4), None);
}
}

View File

@@ -1,22 +1,12 @@
use clap::ValueEnum;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;
#[derive(
Copy,
Clone,
Debug,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
PartialEq,
)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum AnimationStyle {
Linear,
EaseInSine,

View File

@@ -1,7 +1,6 @@
use std::num::NonZeroUsize;
use clap::ValueEnum;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
@@ -603,18 +602,8 @@ impl Arrangement for CustomLayout {
}
}
#[derive(
Clone,
Copy,
Debug,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
PartialEq,
)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum Axis {
Horizontal,
Vertical,

View File

@@ -2,7 +2,6 @@ use crate::config_generation::ApplicationConfiguration;
use crate::config_generation::ApplicationOptions;
use crate::config_generation::MatchingRule;
use color_eyre::Result;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeMap;
@@ -10,10 +9,12 @@ use std::ops::Deref;
use std::ops::DerefMut;
use std::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ApplicationSpecificConfiguration(pub BTreeMap<String, AscApplicationRulesOrSchema>);
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum AscApplicationRulesOrSchema {
AscApplicationRules(AscApplicationRules),
@@ -46,7 +47,8 @@ impl ApplicationSpecificConfiguration {
}
/// Rules that determine how an application is handled
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct AscApplicationRules {
/// Rules to ignore specific windows
#[serde(skip_serializing_if = "Option::is_none")]

View File

@@ -1,6 +1,5 @@
use clap::ValueEnum;
use color_eyre::Result;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
@@ -8,9 +7,8 @@ use strum::EnumString;
use super::ApplicationIdentifier;
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum ApplicationOptions {
@@ -52,14 +50,16 @@ impl ApplicationOptions {
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum MatchingRule {
Simple(IdWithIdentifier),
Composite(Vec<IdWithIdentifier>),
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct WorkspaceMatchingRule {
pub monitor_index: usize,
pub workspace_index: usize,
@@ -67,7 +67,8 @@ pub struct WorkspaceMatchingRule {
pub initial_only: bool,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct IdWithIdentifier {
pub kind: ApplicationIdentifier,
pub id: String,
@@ -75,7 +76,8 @@ pub struct IdWithIdentifier {
pub matching_strategy: Option<MatchingStrategy>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Display, JsonSchema)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Display)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum MatchingStrategy {
Legacy,
Equals,
@@ -89,7 +91,8 @@ pub enum MatchingStrategy {
DoesNotContain,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct IdWithIdentifierAndComment {
pub kind: ApplicationIdentifier,
pub id: String,
@@ -109,7 +112,8 @@ impl From<IdWithIdentifierAndComment> for IdWithIdentifier {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ApplicationConfiguration {
pub name: String,
pub identifier: IdWithIdentifier,
@@ -133,7 +137,8 @@ impl ApplicationConfiguration {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ApplicationConfigurationGenerator;
impl ApplicationConfigurationGenerator {

View File

@@ -8,13 +8,13 @@ use std::path::Path;
use color_eyre::eyre::anyhow;
use color_eyre::eyre::bail;
use color_eyre::Result;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use super::Rect;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CustomLayout(Vec<Column>);
impl Deref for CustomLayout {
@@ -250,7 +250,8 @@ impl CustomLayout {
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "column", content = "configuration")]
pub enum Column {
Primary(Option<ColumnWidth>),
@@ -258,18 +259,21 @@ pub enum Column {
Tertiary(ColumnSplit),
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum ColumnWidth {
WidthPercentage(f32),
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum ColumnSplit {
Horizontal,
Vertical,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum ColumnSplitWithCapacity {
Horizontal(usize),
Vertical(usize),

View File

@@ -1,15 +1,13 @@
use std::num::NonZeroUsize;
use clap::ValueEnum;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
use strum::EnumString;
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum CycleDirection {
Previous,
Next,

View File

@@ -1,5 +1,4 @@
use clap::ValueEnum;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
@@ -10,18 +9,9 @@ use super::Rect;
use super::Sizing;
#[derive(
Clone,
Copy,
Debug,
Serialize,
Deserialize,
Eq,
PartialEq,
Display,
EnumString,
ValueEnum,
JsonSchema,
Clone, Copy, Debug, Serialize, Deserialize, Eq, PartialEq, Display, EnumString, ValueEnum,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum DefaultLayout {
BSP,
Columns,

View File

@@ -1,4 +1,3 @@
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
@@ -7,7 +6,8 @@ use super::CustomLayout;
use super::DefaultLayout;
use super::Direction;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum Layout {
Default(DefaultLayout),
Custom(CustomLayout),

View File

@@ -1,14 +1,11 @@
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
use clap::ValueEnum;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
@@ -29,7 +26,10 @@ pub use default_layout::DefaultLayout;
pub use direction::Direction;
pub use layout::Layout;
pub use operation_direction::OperationDirection;
pub use pathext::replace_env_in_path;
pub use pathext::resolve_option_hashmap_usize_path;
pub use pathext::PathExt;
pub use pathext::ResolvedPathBuf;
pub use rect::Rect;
pub mod animation;
@@ -45,7 +45,10 @@ pub mod operation_direction;
pub mod pathext;
pub mod rect;
#[derive(Clone, Debug, Serialize, Deserialize, Display, JsonSchema)]
// serde_as must be before derive
#[serde_with::serde_as]
#[derive(Clone, Debug, Serialize, Deserialize, Display)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "type", content = "content")]
pub enum SocketMessage {
// Window / Container Commands
@@ -62,6 +65,8 @@ pub enum SocketMessage {
UnstackAll,
ResizeWindowEdge(OperationDirection, Sizing),
ResizeWindowAxis(Axis, Sizing),
MoveContainerToLastWorkspace,
SendContainerToLastWorkspace,
MoveContainerToMonitorNumber(usize),
CycleMoveContainerToMonitor(CycleDirection),
MoveContainerToWorkspaceNumber(usize),
@@ -84,6 +89,9 @@ pub enum SocketMessage {
PromoteFocus,
PromoteWindow(OperationDirection),
EagerFocus(String),
LockMonitorWorkspaceContainer(usize, usize, usize),
UnlockMonitorWorkspaceContainer(usize, usize, usize),
ToggleLock,
ToggleFloat,
ToggleMonocle,
ToggleMaximize,
@@ -100,7 +108,7 @@ pub enum SocketMessage {
AdjustWorkspacePadding(Sizing, i32),
ChangeLayout(DefaultLayout),
CycleLayout(CycleDirection),
ChangeLayoutCustom(PathBuf),
ChangeLayoutCustom(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
FlipLayout(Axis),
ToggleWorkspaceWindowContainerBehaviour,
ToggleWorkspaceFloatOverride,
@@ -118,10 +126,11 @@ pub enum SocketMessage {
RetileWithResizeDimensions,
QuickSave,
QuickLoad,
Save(PathBuf),
Load(PathBuf),
Save(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
Load(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
CycleFocusMonitor(CycleDirection),
CycleFocusWorkspace(CycleDirection),
CycleFocusEmptyWorkspace(CycleDirection),
FocusMonitorNumber(usize),
FocusMonitorAtCursor,
FocusLastWorkspace,
@@ -141,23 +150,28 @@ pub enum SocketMessage {
WorkspaceName(usize, usize, String),
WorkspaceLayout(usize, usize, DefaultLayout),
NamedWorkspaceLayout(String, DefaultLayout),
WorkspaceLayoutCustom(usize, usize, PathBuf),
NamedWorkspaceLayoutCustom(String, PathBuf),
WorkspaceLayoutCustom(usize, usize, #[serde_as(as = "ResolvedPathBuf")] PathBuf),
NamedWorkspaceLayoutCustom(String, #[serde_as(as = "ResolvedPathBuf")] PathBuf),
WorkspaceLayoutRule(usize, usize, usize, DefaultLayout),
NamedWorkspaceLayoutRule(String, usize, DefaultLayout),
WorkspaceLayoutCustomRule(usize, usize, usize, PathBuf),
NamedWorkspaceLayoutCustomRule(String, usize, PathBuf),
WorkspaceLayoutCustomRule(
usize,
usize,
usize,
#[serde_as(as = "ResolvedPathBuf")] PathBuf,
),
NamedWorkspaceLayoutCustomRule(String, usize, #[serde_as(as = "ResolvedPathBuf")] PathBuf),
ClearWorkspaceLayoutRules(usize, usize),
ClearNamedWorkspaceLayoutRules(String),
ToggleWorkspaceLayer,
// Configuration
ReloadConfiguration,
ReplaceConfiguration(PathBuf),
ReloadStaticConfiguration(PathBuf),
ReplaceConfiguration(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
ReloadStaticConfiguration(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
WatchConfiguration(bool),
CompleteConfiguration,
AltFocusHack(bool),
Theme(KomorebiTheme),
Theme(Box<KomorebiTheme>),
Animation(bool, Option<AnimationPrefix>),
AnimationDuration(u64, Option<AnimationPrefix>),
AnimationFps(u64),
@@ -196,6 +210,9 @@ pub enum SocketMessage {
ClearNamedWorkspaceRules(String),
ClearAllWorkspaceRules,
EnforceWorkspaceRules,
SessionFloatRule,
SessionFloatRules,
ClearSessionFloatRules,
#[serde(alias = "FloatRule")]
IgnoreRule(ApplicationIdentifier, String),
ManageRule(ApplicationIdentifier, String),
@@ -241,24 +258,23 @@ impl FromStr for SocketMessage {
}
}
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SubscribeOptions {
/// Only emit notifications when the window manager state has changed
pub filter_state_changes: bool,
}
#[derive(
Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema, ValueEnum,
)]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Display, Serialize, Deserialize, ValueEnum)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum StackbarMode {
Always,
Never,
OnStack,
}
#[derive(
Debug, Copy, Default, Clone, Eq, PartialEq, Display, Serialize, Deserialize, JsonSchema,
)]
#[derive(Debug, Copy, Default, Clone, Eq, PartialEq, Display, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum StackbarLabel {
#[default]
Process,
@@ -266,18 +282,9 @@ pub enum StackbarLabel {
}
#[derive(
Default,
Copy,
Clone,
Debug,
Eq,
PartialEq,
Display,
Serialize,
Deserialize,
JsonSchema,
ValueEnum,
Default, Copy, Clone, Debug, Eq, PartialEq, Display, Serialize, Deserialize, ValueEnum,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum BorderStyle {
#[default]
/// Use the system border style
@@ -289,18 +296,9 @@ pub enum BorderStyle {
}
#[derive(
Default,
Copy,
Clone,
Debug,
Eq,
PartialEq,
Display,
Serialize,
Deserialize,
JsonSchema,
ValueEnum,
Default, Copy, Clone, Debug, Eq, PartialEq, Display, Serialize, Deserialize, ValueEnum,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum BorderImplementation {
#[default]
/// Use the adjustable komorebi border implementation
@@ -313,48 +311,43 @@ pub enum BorderImplementation {
Copy,
Clone,
Debug,
Default,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
PartialEq,
Eq,
Hash,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum WindowKind {
Single,
Stack,
Monocle,
#[default]
Unfocused,
UnfocusedLocked,
Floating,
}
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum StateQuery {
FocusedMonitorIndex,
FocusedWorkspaceIndex,
FocusedContainerIndex,
FocusedWindowIndex,
FocusedWorkspaceName,
FocusedWorkspaceLayout,
Version,
}
#[derive(
Copy,
Clone,
Debug,
Eq,
PartialEq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum ApplicationIdentifier {
#[serde(alias = "exe")]
Exe,
@@ -366,18 +359,8 @@ pub enum ApplicationIdentifier {
Path,
}
#[derive(
Copy,
Clone,
Debug,
PartialEq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
)]
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum FocusFollowsMouseImplementation {
/// A custom FFM implementation (slightly more CPU-intensive)
Komorebi,
@@ -385,7 +368,8 @@ pub enum FocusFollowsMouseImplementation {
Windows,
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct WindowManagementBehaviour {
/// The current WindowContainerBehaviour to be used
pub current_behaviour: WindowContainerBehaviour,
@@ -396,18 +380,9 @@ pub struct WindowManagementBehaviour {
}
#[derive(
Clone,
Copy,
Debug,
Default,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
PartialEq,
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum WindowContainerBehaviour {
/// Create a new container for each new window
#[default]
@@ -417,17 +392,19 @@ pub enum WindowContainerBehaviour {
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
Clone, Copy, Debug, Default, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum FloatingLayerBehaviour {
/// Tile new windows (unless they match a float rule)
#[default]
Tile,
/// Float new windows
Float,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum MoveBehaviour {
/// Swap the window container with the window container at the edge of the adjacent monitor
Swap,
@@ -437,18 +414,8 @@ pub enum MoveBehaviour {
NoOp,
}
#[derive(
Clone,
Copy,
Debug,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
PartialEq,
)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum CrossBoundaryBehaviour {
/// Attempt to perform actions across a workspace boundary
Workspace,
@@ -456,18 +423,8 @@ pub enum CrossBoundaryBehaviour {
Monitor,
}
#[derive(
Copy,
Clone,
Debug,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
PartialEq,
)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum HidingBehaviour {
/// Use the SW_HIDE flag to hide windows when switching workspaces (has issues with Electron apps)
Hide,
@@ -477,18 +434,8 @@ pub enum HidingBehaviour {
Cloak,
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Serialize,
Deserialize,
Display,
EnumString,
ValueEnum,
JsonSchema,
)]
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, Display, EnumString, ValueEnum)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum OperationBehaviour {
/// Process komorebic commands on temporarily unmanaged/floated windows
Op,
@@ -496,9 +443,8 @@ pub enum OperationBehaviour {
NoOp,
}
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum Sizing {
Increase,
Decrease,
@@ -520,45 +466,28 @@ impl Sizing {
}
}
pub fn resolve_home_path<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
let mut resolved_path = PathBuf::new();
let mut resolved = false;
for c in path.as_ref().components() {
match c {
std::path::Component::Normal(c)
if (c == "~" || c == "$Env:USERPROFILE" || c == "$HOME") && !resolved =>
{
let home = dirs::home_dir().ok_or_else(|| anyhow!("there is no home directory"))?;
#[cfg(test)]
mod tests {
use super::*;
resolved_path.extend(home.components());
resolved = true;
}
#[test]
fn deserializes() {
// Set a variable for testing
std::env::set_var("VAR", "VALUE");
std::path::Component::Normal(c) if (c == "$Env:KOMOREBI_CONFIG_HOME") && !resolved => {
let komorebi_config_home =
PathBuf::from(std::env::var("KOMOREBI_CONFIG_HOME").ok().ok_or_else(|| {
anyhow!("there is no KOMOREBI_CONFIG_HOME environment variable set")
})?);
let json = r#"{"type":"WorkspaceLayoutCustomRule","content":[0,0,0,"/path/%VAR%/d"]}"#;
let message: SocketMessage = serde_json::from_str(json).unwrap();
resolved_path.extend(komorebi_config_home.components());
resolved = true;
}
let SocketMessage::WorkspaceLayoutCustomRule(
_workspace_index,
_workspace_number,
_monitor_index,
path,
) = message
else {
panic!("Expected WorkspaceLayoutCustomRule");
};
_ => resolved_path.push(c),
}
assert_eq!(path, PathBuf::from("/path/VALUE/d"));
}
let parent = resolved_path
.parent()
.ok_or_else(|| anyhow!("cannot parse parent directory"))?;
Ok(if parent.is_dir() {
let file = resolved_path
.components()
.last()
.ok_or_else(|| anyhow!("cannot parse filename"))?;
dunce::canonicalize(parent)?.join(file)
} else {
resolved_path
})
}

View File

@@ -1,7 +1,6 @@
use std::num::NonZeroUsize;
use clap::ValueEnum;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
@@ -10,9 +9,8 @@ use strum::EnumString;
use super::direction::Direction;
use super::Axis;
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum, JsonSchema,
)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Display, EnumString, ValueEnum)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum OperationDirection {
Left,
Right,

View File

@@ -1,48 +1,192 @@
use std::collections::HashMap;
use std::env;
use std::ffi::OsStr;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use serde::Deserialize;
use serde::Serialize;
/// Path extension trait
pub trait PathExt {
/// Resolve environment variables components in a path.
///
/// Resolves the follwing formats:
/// - CMD: `%variable%`
/// - PowerShell: `$Env:variable`
/// - Bash: `$variable`.
fn replace_env(&self) -> PathBuf;
}
impl PathExt for PathBuf {
/// Blanket implementation for all types that can be converted to a `Path`.
impl<P: AsRef<Path>> PathExt for P {
fn replace_env(&self) -> PathBuf {
let mut result = PathBuf::new();
let mut out = PathBuf::new();
for component in self.components() {
match component {
Component::Normal(segment) => {
// Check if it starts with `$` or `$Env:`
if let Some(stripped_segment) = segment.to_string_lossy().strip_prefix('$') {
let var_name = if let Some(env_name) = stripped_segment.strip_prefix("Env:")
{
// Extract the variable name after `$Env:`
env_name
} else if stripped_segment == "HOME" {
// Special case for `$HOME`
"USERPROFILE"
} else {
// Extract the variable name after `$`
stripped_segment
};
if let Ok(value) = env::var(var_name) {
result.push(&value); // Replace with the value
} else {
result.push(segment); // Keep as-is if variable is not found
}
} else {
result.push(segment); // Keep as-is if not an environment variable
for c in self.as_ref().components() {
match c {
Component::Normal(mut c) => {
// Special case for ~ and $HOME, replace with $Env:USERPROFILE
if c == OsStr::new("~") || c.eq_ignore_ascii_case("$HOME") {
c = OsStr::new("$Env:USERPROFILE");
}
let bytes = c.as_encoded_bytes();
// %LOCALAPPDATA%
let var = if bytes[0] == b'%' && bytes[bytes.len() - 1] == b'%' {
Some(&bytes[1..bytes.len() - 1])
} else {
// prefix length is 5 for $Env: and 1 for $
// so we take the minimum of 5 and the length of the bytes
let prefix = &bytes[..5.min(bytes.len())];
let prefix = unsafe { OsStr::from_encoded_bytes_unchecked(prefix) };
// $Env:LOCALAPPDATA
if prefix.eq_ignore_ascii_case("$Env:") {
Some(&bytes[5..])
} else if bytes[0] == b'$' {
// $LOCALAPPDATA
Some(&bytes[1..])
} else {
// not a variable
None
}
};
// if component is a variable, get the value from the environment
if let Some(var) = var {
let var = unsafe { OsStr::from_encoded_bytes_unchecked(var) };
if let Some(value) = env::var_os(var) {
out.push(value);
continue;
}
}
// if not a variable, or a value couldn't be obtained from environemnt
// then push the component as is
out.push(c);
}
_ => {
// Add other components (e.g., root, parent) as-is
result.push(component.as_os_str());
}
// other components are pushed as is
_ => out.push(c),
}
}
result
out
}
}
/// Replace environment variables in a path. This is a wrapper around
/// [`PathExt::replace_env`] to be used in Clap arguments parsing.
pub fn replace_env_in_path(input: &str) -> Result<PathBuf, std::convert::Infallible> {
Ok(input.replace_env())
}
/// A wrapper around [`PathBuf`] that has a custom [Deserialize] implementation
/// that uses [`PathExt::replace_env`] to resolve environment variables
#[derive(Clone, Debug)]
pub struct ResolvedPathBuf(PathBuf);
impl ResolvedPathBuf {
/// Create a new [`ResolvedPathBuf`] from a [`PathBuf`]
pub fn new(path: PathBuf) -> Self {
Self(path.replace_env())
}
}
impl From<ResolvedPathBuf> for PathBuf {
fn from(path: ResolvedPathBuf) -> Self {
path.0
}
}
impl serde_with::SerializeAs<PathBuf> for ResolvedPathBuf {
fn serialize_as<S>(path: &PathBuf, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
path.serialize(serializer)
}
}
impl<'de> serde_with::DeserializeAs<'de, PathBuf> for ResolvedPathBuf {
fn deserialize_as<D>(deserializer: D) -> Result<PathBuf, D::Error>
where
D: serde::Deserializer<'de>,
{
let path = PathBuf::deserialize(deserializer)?;
Ok(path.replace_env())
}
}
#[cfg(feature = "schemars")]
impl serde_with::schemars_0_8::JsonSchemaAs<PathBuf> for ResolvedPathBuf {
fn schema_name() -> String {
"PathBuf".to_owned()
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
<PathBuf as schemars::JsonSchema>::json_schema(gen)
}
}
/// Custom deserializer for [`Option<HashMap<usize, PathBuf>>`] that uses
/// [`PathExt::replace_env`] to resolve environment variables in the paths.
///
/// This is used in `WorkspaceConfig` struct because we can't use
/// #[serde_with::serde_as] as it doesn't handle [`Option<HashMap<usize, ResolvedPathBuf>>`]
/// quite well and generated compiler errors that can't be fixed because of Rust's orphan rule.
pub fn resolve_option_hashmap_usize_path<'de, D>(
deserializer: D,
) -> Result<Option<HashMap<usize, PathBuf>>, D::Error>
where
D: serde::Deserializer<'de>,
{
let map = Option::<HashMap<usize, PathBuf>>::deserialize(deserializer)?;
Ok(map.map(|map| map.into_iter().map(|(k, v)| (k, v.replace_env())).collect()))
}
#[cfg(test)]
mod tests {
use super::*;
// helper functions
fn expected<P: AsRef<Path>>(p: P) -> PathBuf {
// Ensure that the path is using the correct path separator for the OS.
p.as_ref().components().collect::<PathBuf>()
}
fn resolve<P: AsRef<Path>>(p: P) -> PathBuf {
p.replace_env()
}
#[test]
fn resolves_env_vars() {
// Set a variable for testing
std::env::set_var("VAR", "VALUE");
// %VAR% format
assert_eq!(resolve("/path/%VAR%/d"), expected("/path/VALUE/d"));
// $env:VAR format
assert_eq!(resolve("/path/$env:VAR/d"), expected("/path/VALUE/d"));
// $VAR format
assert_eq!(resolve("/path/$VAR/d"), expected("/path/VALUE/d"));
// non-existent variable
assert_eq!(resolve("/path/%ASD%/to/d"), expected("/path/%ASD%/to/d"));
assert_eq!(
resolve("/path/$env:ASD/to/d"),
expected("/path/$env:ASD/to/d")
);
assert_eq!(resolve("/path/$ASD/to/d"), expected("/path/$ASD/to/d"));
// Set a $env:USERPROFILE variable for testing
std::env::set_var("USERPROFILE", "C:\\Users\\user");
// ~ and $HOME should be replaced with $Env:USERPROFILE
assert_eq!(resolve("~"), expected("C:\\Users\\user"));
assert_eq!(resolve("$HOME"), expected("C:\\Users\\user"));
}
}

View File

@@ -1,9 +1,9 @@
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use windows::Win32::Foundation::RECT;
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Eq, PartialEq, JsonSchema)]
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Rect {
/// The left point in a Win32 Rect
pub left: i32,

View File

@@ -5,10 +5,10 @@ pub mod border_manager;
pub mod com;
#[macro_use]
pub mod ring;
pub mod colour;
pub mod container;
pub mod core;
pub mod focus_manager;
pub mod locked_deque;
pub mod monitor;
pub mod monitor_reconciliator;
pub mod process_command;
@@ -29,7 +29,6 @@ pub mod windows_callbacks;
pub mod winevent;
pub mod winevent_listener;
pub mod workspace;
pub mod workspace_reconciliator;
use lazy_static::lazy_static;
use monitor_reconciliator::MonitorNotification;
@@ -47,11 +46,12 @@ use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::sync::Arc;
pub use colour::*;
pub use core::*;
pub use komorebi_themes::colour::*;
pub use process_command::*;
pub use process_event::*;
pub use static_config::*;
pub use win32_display_data;
pub use window::*;
pub use window_manager::*;
pub use window_manager_event::*;
@@ -65,8 +65,8 @@ use crate::core::config_generation::WorkspaceMatchingRule;
use color_eyre::Result;
use os_info::Version;
use parking_lot::Mutex;
use parking_lot::RwLock;
use regex::Regex;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use uds_windows::UnixStream;
@@ -132,8 +132,8 @@ lazy_static! {
static ref TRANSPARENCY_BLACKLIST: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(Vec::new()));
static ref MONITOR_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, Rect>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref DISPLAY_INDEX_PREFERENCES: Arc<Mutex<HashMap<usize, String>>> =
Arc::new(Mutex::new(HashMap::new()));
static ref DISPLAY_INDEX_PREFERENCES: Arc<RwLock<HashMap<usize, String>>> =
Arc::new(RwLock::new(HashMap::new()));
static ref WORKSPACE_MATCHING_RULES: Arc<Mutex<Vec<WorkspaceMatchingRule>>> =
Arc::new(Mutex::new(Vec::new()));
static ref REGEX_IDENTIFIERS: Arc<Mutex<HashMap<String, Regex>>> =
@@ -158,6 +158,7 @@ lazy_static! {
matching_strategy: Option::from(MatchingStrategy::Equals),
})
]));
static ref SESSION_FLOATING_APPLICATIONS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(Vec::new()));
static ref FLOATING_APPLICATIONS: Arc<Mutex<Vec<MatchingRule>>> = Arc::new(Mutex::new(Vec::new()));
static ref PERMAIGNORE_CLASSES: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
"Chrome_RenderWidgetHostHWND".to_string(),
@@ -173,6 +174,8 @@ lazy_static! {
matching_strategy: Option::from(MatchingStrategy::Equals),
}),
]));
static ref DUPLICATE_MONITOR_SERIAL_IDS: Arc<RwLock<Vec<String>>> =
Arc::new(RwLock::new(Vec::new()));
static ref SUBSCRIPTION_PIPES: Arc<Mutex<HashMap<String, File>>> =
Arc::new(Mutex::new(HashMap::new()));
pub static ref SUBSCRIPTION_SOCKETS: Arc<Mutex<HashMap<String, PathBuf>>> =
@@ -185,15 +188,16 @@ lazy_static! {
Arc::new(Mutex::new(HidingBehaviour::Cloak));
pub static ref HOME_DIR: PathBuf = {
std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(|_| dirs::home_dir().expect("there is no home directory"), |home_path| {
let home = PathBuf::from(&home_path);
let home = home_path.replace_env();
if home.as_path().is_dir() {
home
} else {
panic!(
"$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory",
);
}
assert!(
home.is_dir(),
"$Env:KOMOREBI_CONFIG_HOME is set to '{}', which is not a valid directory",
home_path
);
home
})
};
pub static ref DATA_DIR: PathBuf = dirs::data_local_dir().expect("there is no local data directory").join("komorebi");
@@ -236,6 +240,8 @@ pub static REMOVE_TITLEBARS: AtomicBool = AtomicBool::new(false);
pub static SLOW_APPLICATION_COMPENSATION_TIME: AtomicU64 = AtomicU64::new(20);
shadow_rs::shadow!(build);
#[must_use]
pub fn current_virtual_desktop() -> Option<Vec<u8>> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
@@ -280,7 +286,8 @@ pub fn current_virtual_desktop() -> Option<Vec<u8>> {
current
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum NotificationEvent {
WindowManager(WindowManagerEvent),
@@ -288,7 +295,8 @@ pub enum NotificationEvent {
Monitor(MonitorNotification),
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Notification {
pub event: NotificationEvent,
pub state: State,

View File

@@ -0,0 +1,316 @@
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
}
}
}

View File

@@ -16,14 +16,17 @@ use std::sync::Arc;
use std::time::Duration;
use clap::Parser;
use clap::ValueEnum;
use color_eyre::Result;
use crossbeam_utils::Backoff;
use komorebi::animation::AnimationEngine;
use komorebi::animation::ANIMATION_ENABLED_GLOBAL;
use komorebi::animation::ANIMATION_ENABLED_PER_ANIMATION;
use komorebi::replace_env_in_path;
#[cfg(feature = "deadlock_detection")]
use parking_lot::deadlock;
use parking_lot::Mutex;
use serde::Deserialize;
use sysinfo::Process;
use sysinfo::ProcessesToUpdate;
use tracing_appender::non_blocking::WorkerGuard;
@@ -48,16 +51,13 @@ use komorebi::window_manager::State;
use komorebi::window_manager::WindowManager;
use komorebi::windows_api::WindowsApi;
use komorebi::winevent_listener;
use komorebi::workspace_reconciliator;
use komorebi::CUSTOM_FFM;
use komorebi::DATA_DIR;
use komorebi::HOME_DIR;
use komorebi::INITIAL_CONFIGURATION_LOADED;
use komorebi::SESSION_ID;
shadow_rs::shadow!(build);
fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
fn setup(log_level: LogLevel) -> Result<(WorkerGuard, WorkerGuard)> {
if std::env::var("RUST_LIB_BACKTRACE").is_err() {
std::env::set_var("RUST_LIB_BACKTRACE", "1");
}
@@ -65,7 +65,16 @@ fn setup() -> Result<(WorkerGuard, WorkerGuard)> {
color_eyre::install()?;
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
std::env::set_var(
"RUST_LOG",
match log_level {
LogLevel::Error => "error",
LogLevel::Warn => "warn",
LogLevel::Info => "info",
LogLevel::Debug => "debug",
LogLevel::Trace => "trace",
},
);
}
let appender = tracing_appender::rolling::daily(std::env::temp_dir(), "komorebi_plaintext.log");
@@ -143,8 +152,19 @@ fn detect_deadlocks() {
});
}
#[derive(Default, Deserialize, ValueEnum, Clone)]
#[serde(rename_all = "snake_case")]
enum LogLevel {
Error,
Warn,
#[default]
Info,
Debug,
Trace,
}
#[derive(Parser)]
#[clap(author, about, version = build::CLAP_LONG_VERSION)]
#[clap(author, about, version = komorebi::build::CLAP_LONG_VERSION)]
struct Opts {
/// Allow the use of komorebi's custom focus-follows-mouse implementation
#[clap(short, long = "ffm")]
@@ -157,10 +177,14 @@ struct Opts {
tcp_port: Option<usize>,
/// Path to a static configuration JSON file
#[clap(short, long)]
#[clap(value_parser = replace_env_in_path)]
config: Option<PathBuf>,
/// Do not attempt to auto-apply a dumped state temp file from a previously running instance of komorebi
#[clap(long)]
clean_state: bool,
/// Level of log output verbosity
#[clap(long, value_enum, default_value_t=LogLevel::Info)]
log_level: LogLevel,
}
#[tracing::instrument]
@@ -198,7 +222,7 @@ fn main() -> Result<()> {
}
// File logging worker guard has to have an assignment in the main fn to work
let (_guard, _color_guard) = setup()?;
let (_guard, _color_guard) = setup(opts.log_level)?;
WindowsApi::foreground_lock_timeout()?;
@@ -235,6 +259,7 @@ fn main() -> Result<()> {
} else {
Arc::new(Mutex::new(WindowManager::new(
winevent_listener::event_rx(),
None,
)?))
};
@@ -277,7 +302,6 @@ fn main() -> Result<()> {
border_manager::listen_for_notifications(wm.clone());
stackbar_manager::listen_for_notifications(wm.clone());
transparency_manager::listen_for_notifications(wm.clone());
workspace_reconciliator::listen_for_notifications(wm.clone());
monitor_reconciliator::listen_for_notifications(wm.clone())?;
reaper::listen_for_notifications(wm.clone(), wm.lock().known_hwnds.clone());
focus_manager::listen_for_notifications(wm.clone());

View File

@@ -9,34 +9,32 @@ use getset::CopyGetters;
use getset::Getters;
use getset::MutGetters;
use getset::Setters;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use crate::border_manager::BORDER_ENABLED;
use crate::border_manager::BORDER_OFFSET;
use crate::border_manager::BORDER_WIDTH;
use crate::core::Rect;
use crate::container::Container;
use crate::ring::Ring;
use crate::workspace::Workspace;
use crate::workspace::WorkspaceGlobals;
use crate::workspace::WorkspaceLayer;
use crate::DefaultLayout;
use crate::FloatingLayerBehaviour;
use crate::Layout;
use crate::OperationDirection;
use crate::Wallpaper;
use crate::WindowsApi;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::DEFAULT_WORKSPACE_PADDING;
#[derive(
Debug,
Clone,
Serialize,
Deserialize,
Getters,
CopyGetters,
MutGetters,
Setters,
JsonSchema,
PartialEq,
Debug, Clone, Serialize, Deserialize, Getters, CopyGetters, MutGetters, Setters, PartialEq,
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Monitor {
#[getset(get_copy = "pub", set = "pub")]
pub id: isize,
@@ -68,6 +66,10 @@ pub struct Monitor {
pub container_padding: Option<i32>,
#[getset(get_copy = "pub", set = "pub")]
pub workspace_padding: Option<i32>,
#[getset(get = "pub", get_mut = "pub", set = "pub")]
pub wallpaper: Option<Wallpaper>,
#[getset(get_copy = "pub", set = "pub")]
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
}
impl_ring_elements!(Monitor, Workspace);
@@ -123,6 +125,8 @@ pub fn new(
workspace_names: HashMap::default(),
container_padding: None,
workspace_padding: None,
wallpaper: None,
floating_layer_behaviour: None,
}
}
@@ -164,6 +168,8 @@ impl Monitor {
workspace_names: Default::default(),
container_padding: None,
workspace_padding: None,
wallpaper: None,
floating_layer_behaviour: None,
}
}
@@ -173,11 +179,23 @@ impl Monitor {
.unwrap_or(None)
}
pub fn focused_workspace_layout(&self) -> Option<Layout> {
self.focused_workspace().and_then(|workspace| {
if *workspace.tile() {
Some(workspace.layout().clone())
} else {
None
}
})
}
pub fn load_focused_workspace(&mut self, mouse_follows_focus: bool) -> Result<()> {
let focused_idx = self.focused_workspace_idx();
let hmonitor = self.id();
let monitor_wp = self.wallpaper.clone();
for (i, workspace) in self.workspaces_mut().iter_mut().enumerate() {
if i == focused_idx {
workspace.restore(mouse_follows_focus)?;
workspace.restore(mouse_follows_focus, hmonitor, &monitor_wp)?;
} else {
workspace.hide(None);
}
@@ -194,18 +212,34 @@ impl Monitor {
let workspace_padding = self
.workspace_padding()
.or(Some(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)));
let (border_width, border_offset) = {
let border_enabled = BORDER_ENABLED.load(Ordering::SeqCst);
if border_enabled {
let border_width = BORDER_WIDTH.load(Ordering::SeqCst);
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
(border_width, border_offset)
} else {
(0, 0)
}
};
let work_area = *self.work_area_size();
let offset = self.work_area_offset.or(offset);
let work_area_offset = self.work_area_offset.or(offset);
let window_based_work_area_offset = self.window_based_work_area_offset();
let limit = self.window_based_work_area_offset_limit();
let window_based_work_area_offset_limit = self.window_based_work_area_offset_limit();
let floating_layer_behaviour = self.floating_layer_behaviour();
for workspace in self.workspaces_mut() {
workspace.globals_mut().container_padding = container_padding;
workspace.globals_mut().workspace_padding = workspace_padding;
workspace.globals_mut().work_area = work_area;
workspace.globals_mut().work_area_offset = offset;
workspace.globals_mut().window_based_work_area_offset = window_based_work_area_offset;
workspace.globals_mut().window_based_work_area_offset_limit = limit;
workspace.globals = WorkspaceGlobals {
container_padding,
workspace_padding,
border_width,
border_offset,
work_area,
work_area_offset,
window_based_work_area_offset,
window_based_work_area_offset_limit,
floating_layer_behaviour,
}
}
}
@@ -217,18 +251,34 @@ impl Monitor {
let workspace_padding = self
.workspace_padding()
.or(Some(DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)));
let (border_width, border_offset) = {
let border_enabled = BORDER_ENABLED.load(Ordering::SeqCst);
if border_enabled {
let border_width = BORDER_WIDTH.load(Ordering::SeqCst);
let border_offset = BORDER_OFFSET.load(Ordering::SeqCst);
(border_width, border_offset)
} else {
(0, 0)
}
};
let work_area = *self.work_area_size();
let offset = self.work_area_offset.or(offset);
let work_area_offset = self.work_area_offset.or(offset);
let window_based_work_area_offset = self.window_based_work_area_offset();
let limit = self.window_based_work_area_offset_limit();
let window_based_work_area_offset_limit = self.window_based_work_area_offset_limit();
let floating_layer_behaviour = self.floating_layer_behaviour();
if let Some(workspace) = self.workspaces_mut().get_mut(workspace_idx) {
workspace.globals_mut().container_padding = container_padding;
workspace.globals_mut().workspace_padding = workspace_padding;
workspace.globals_mut().work_area = work_area;
workspace.globals_mut().work_area_offset = offset;
workspace.globals_mut().window_based_work_area_offset = window_based_work_area_offset;
workspace.globals_mut().window_based_work_area_offset_limit = limit;
workspace.globals = WorkspaceGlobals {
container_padding,
workspace_padding,
border_width,
border_offset,
work_area,
work_area_offset,
window_based_work_area_offset,
window_based_work_area_offset_limit,
floating_layer_behaviour,
}
}
}
@@ -378,19 +428,20 @@ impl Monitor {
.position(|w| w.hwnd == foreground_hwnd);
if let Some(idx) = floating_window_index {
let window = workspace.floating_windows_mut().remove(idx);
if let Some(window) = workspace.floating_windows_mut().remove(idx) {
let workspaces = self.workspaces_mut();
#[allow(clippy::option_if_let_else)]
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
None => {
workspaces.resize(target_workspace_idx + 1, Workspace::default());
workspaces.get_mut(target_workspace_idx).unwrap()
}
Some(workspace) => workspace,
};
let workspaces = self.workspaces_mut();
#[allow(clippy::option_if_let_else)]
let target_workspace = match workspaces.get_mut(target_workspace_idx) {
None => {
workspaces.resize(target_workspace_idx + 1, Workspace::default());
workspaces.get_mut(target_workspace_idx).unwrap()
}
Some(workspace) => workspace,
};
target_workspace.floating_windows_mut().push(window);
target_workspace.floating_windows_mut().push_back(window);
target_workspace.set_layer(WorkspaceLayer::Floating);
}
} else {
let container = workspace
.remove_focused_container()
@@ -407,6 +458,8 @@ impl Monitor {
Some(workspace) => workspace,
};
target_workspace.set_layer(WorkspaceLayer::Tiling);
if let Some(direction) = direction {
self.add_container_with_direction(
container,
@@ -473,3 +526,252 @@ impl Monitor {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_container() {
let mut m = Monitor::new(
0,
Rect::default(),
Rect::default(),
"TestMonitor".to_string(),
"TestDevice".to_string(),
"TestDeviceID".to_string(),
Some("TestMonitorID".to_string()),
);
// Add container to the default workspace
m.add_container(Container::default(), Some(0)).unwrap();
// Should contain a container in the current focused workspace
let workspace = m.focused_workspace_mut().unwrap();
assert_eq!(workspace.containers().len(), 1);
}
#[test]
fn test_remove_workspace_by_idx() {
let mut m = Monitor::new(
0,
Rect::default(),
Rect::default(),
"TestMonitor".to_string(),
"TestDevice".to_string(),
"TestDeviceID".to_string(),
Some("TestMonitorID".to_string()),
);
let new_workspace_index = m.new_workspace_idx();
assert_eq!(new_workspace_index, 1);
// Create workspace 2
m.focus_workspace(new_workspace_index).unwrap();
// Should have 2 workspaces
assert_eq!(m.workspaces().len(), 2);
// Create workspace 3
m.focus_workspace(new_workspace_index + 1).unwrap();
// Should have 3 workspaces
assert_eq!(m.workspaces().len(), 3);
// Remove workspace 1
m.remove_workspace_by_idx(1);
// Should have only 2 workspaces
assert_eq!(m.workspaces().len(), 2);
}
#[test]
fn test_remove_workspaces() {
let mut m = Monitor::new(
0,
Rect::default(),
Rect::default(),
"TestMonitor".to_string(),
"TestDevice".to_string(),
"TestDeviceID".to_string(),
Some("TestMonitorID".to_string()),
);
let new_workspace_index = m.new_workspace_idx();
assert_eq!(new_workspace_index, 1);
// Create workspace 2
m.focus_workspace(new_workspace_index).unwrap();
// Should have 2 workspaces
assert_eq!(m.workspaces().len(), 2);
// Create workspace 3
m.focus_workspace(new_workspace_index + 1).unwrap();
// Should have 3 workspaces
assert_eq!(m.workspaces().len(), 3);
// Remove all workspaces
m.remove_workspaces();
// All workspaces should be removed
assert_eq!(m.workspaces().len(), 0);
}
#[test]
fn test_focus_workspace() {
let mut m = Monitor::new(
0,
Rect::default(),
Rect::default(),
"TestMonitor".to_string(),
"TestDevice".to_string(),
"TestDeviceID".to_string(),
Some("TestMonitorID".to_string()),
);
let new_workspace_index = m.new_workspace_idx();
assert_eq!(new_workspace_index, 1);
// Focus workspace 2
m.focus_workspace(new_workspace_index).unwrap();
// Should have 2 workspaces
assert_eq!(m.workspaces().len(), 2);
// Should be focused on workspace 2
assert_eq!(m.focused_workspace_idx(), 1);
}
#[test]
fn test_new_workspace_idx() {
let m = Monitor::new(
0,
Rect::default(),
Rect::default(),
"TestMonitor".to_string(),
"TestDevice".to_string(),
"TestDeviceID".to_string(),
Some("TestMonitorID".to_string()),
);
let new_workspace_index = m.new_workspace_idx();
// Should be the last workspace index: 1
assert_eq!(new_workspace_index, 1);
}
#[test]
fn test_move_container_to_workspace() {
let mut m = Monitor::new(
0,
Rect::default(),
Rect::default(),
"TestMonitor".to_string(),
"TestDevice".to_string(),
"TestDeviceID".to_string(),
Some("TestMonitorID".to_string()),
);
let new_workspace_index = m.new_workspace_idx();
assert_eq!(new_workspace_index, 1);
{
// Create workspace 1 and add 3 containers
let workspace = m.focused_workspace_mut().unwrap();
for _ in 0..3 {
let container = Container::default();
workspace.add_container_to_back(container);
}
// Should have 3 containers in workspace 1
assert_eq!(m.focused_workspace().unwrap().containers().len(), 3);
}
// Create and focus workspace 2
m.focus_workspace(new_workspace_index).unwrap();
// Focus workspace 1
m.focus_workspace(0).unwrap();
// Move container to workspace 2
m.move_container_to_workspace(1, true, None).unwrap();
// Should be focused on workspace 2
assert_eq!(m.focused_workspace_idx(), 1);
// Workspace 2 should have 1 container now
assert_eq!(m.focused_workspace().unwrap().containers().len(), 1);
// Move to workspace 1
m.focus_workspace(0).unwrap();
// Workspace 1 should have 2 containers
assert_eq!(m.focused_workspace().unwrap().containers().len(), 2);
// Move a another container from workspace 1 to workspace 2 without following
m.move_container_to_workspace(1, false, None).unwrap();
// Should have 1 container
assert_eq!(m.focused_workspace().unwrap().containers().len(), 1);
// Should still be focused on workspace 1
assert_eq!(m.focused_workspace_idx(), 0);
// Switch to workspace 2
m.focus_workspace(1).unwrap();
// Workspace 2 should now have 2 containers
assert_eq!(m.focused_workspace().unwrap().containers().len(), 2);
}
#[test]
fn test_ensure_workspace_count_workspace_contains_two_workspaces() {
let mut m = Monitor::new(
0,
Rect::default(),
Rect::default(),
"TestMonitor".to_string(),
"TestDevice".to_string(),
"TestDeviceID".to_string(),
Some("TestMonitorID".to_string()),
);
// Create and focus another workspace
let new_workspace_index = m.new_workspace_idx();
m.focus_workspace(new_workspace_index).unwrap();
// Should have 2 workspaces now
assert_eq!(m.workspaces().len(), 2, "Monitor should have 2 workspaces");
// Ensure the monitor has at least 5 workspaces
m.ensure_workspace_count(5);
// Monitor should have 5 workspaces
assert_eq!(m.workspaces().len(), 5, "Monitor should have 5 workspaces");
}
#[test]
fn test_ensure_workspace_count_only_default_workspace() {
let mut m = Monitor::new(
0,
Rect::default(),
Rect::default(),
"TestMonitor".to_string(),
"TestDevice".to_string(),
"TestDeviceID".to_string(),
Some("TestMonitorID".to_string()),
);
// Ensure the monitor has at least 5 workspaces
m.ensure_workspace_count(5);
// Monitor should have 5 workspaces
assert_eq!(m.workspaces().len(), 5, "Monitor should have 5 workspaces");
// Try to call the ensure workspace count again to ensure it doesn't change
m.ensure_workspace_count(3);
assert_eq!(m.workspaces().len(), 5, "Monitor should have 5 workspaces");
}
}

View File

@@ -12,12 +12,13 @@ use crate::NotificationEvent;
use crate::State;
use crate::WindowManager;
use crate::WindowsApi;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::DUPLICATE_MONITOR_SERIAL_IDS;
use crate::WORKSPACE_MATCHING_RULES;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicConsume;
use parking_lot::Mutex;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
@@ -28,7 +29,8 @@ use std::sync::OnceLock;
pub mod hidden;
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "type", content = "content")]
pub enum MonitorNotification {
ResolutionScalingChanged,
@@ -66,17 +68,51 @@ pub fn send_notification(notification: MonitorNotification) {
}
pub fn insert_in_monitor_cache(serial_or_device_id: &str, monitor: Monitor) {
let dip = DISPLAY_INDEX_PREFERENCES.read();
let mut dip_ids = dip.values();
let preferred_id = if dip_ids.any(|id| id == monitor.device_id()) {
monitor.device_id().clone()
} else if dip_ids.any(|id| Some(id) == monitor.serial_number_id().as_ref()) {
monitor.serial_number_id().clone().unwrap_or_default()
} else {
serial_or_device_id.to_string()
};
let mut monitor_cache = MONITOR_CACHE
.get_or_init(|| Mutex::new(HashMap::new()))
.lock();
monitor_cache.insert(serial_or_device_id.to_string(), monitor);
monitor_cache.insert(preferred_id, monitor);
}
pub fn attached_display_devices() -> color_eyre::Result<Vec<Monitor>> {
Ok(win32_display_data::connected_displays_all()
.flatten()
.map(|display| {
pub fn attached_display_devices<F, I>(display_provider: F) -> color_eyre::Result<Vec<Monitor>>
where
F: Fn() -> I + Copy,
I: Iterator<Item = Result<win32_display_data::Device, win32_display_data::Error>>,
{
let all_displays = display_provider().flatten().collect::<Vec<_>>();
let mut serial_id_map = HashMap::new();
for d in &all_displays {
if let Some(id) = &d.serial_number_id {
*serial_id_map.entry(id.clone()).or_insert(0) += 1;
}
}
for d in &all_displays {
if let Some(id) = &d.serial_number_id {
if serial_id_map.get(id).copied().unwrap_or_default() > 1 {
let mut dupes = DUPLICATE_MONITOR_SERIAL_IDS.write();
if !dupes.contains(id) {
(*dupes).push(id.clone());
}
}
}
}
Ok(all_displays
.into_iter()
.map(|mut display| {
let path = display.device_path;
let (device, device_id) = if path.is_empty() {
@@ -93,6 +129,13 @@ pub fn attached_display_devices() -> color_eyre::Result<Vec<Monitor>> {
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
if let Some(id) = &display.serial_number_id {
let dupes = DUPLICATE_MONITOR_SERIAL_IDS.read();
if dupes.contains(id) {
display.serial_number_id = None;
}
}
monitor::new(
display.hmonitor,
display.size.into(),
@@ -113,7 +156,7 @@ pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Re
tracing::info!("created hidden window to listen for monitor-related events");
std::thread::spawn(move || loop {
match handle_notifications(wm.clone()) {
match handle_notifications(wm.clone(), win32_display_data::connected_displays_all) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
@@ -130,7 +173,14 @@ pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Re
Ok(())
}
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
pub fn handle_notifications<F, I>(
wm: Arc<Mutex<WindowManager>>,
display_provider: F,
) -> color_eyre::Result<()>
where
F: Fn() -> I + Copy,
I: Iterator<Item = Result<win32_display_data::Device, win32_display_data::Error>>,
{
tracing::info!("listening");
let receiver = event_rx();
@@ -255,14 +305,20 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
let initial_monitor_count = wm.monitors().len();
// Get the currently attached display devices
let attached_devices = attached_display_devices()?;
let attached_devices = attached_display_devices(display_provider)?;
// Make sure that in our state any attached displays have the latest Win32 data
for monitor in wm.monitors_mut() {
for attached in &attached_devices {
if attached.serial_number_id().eq(monitor.serial_number_id())
|| attached.device_id().eq(monitor.device_id())
let serial_number_ids_match = if let (Some(attached_snid), Some(m_snid)) =
(attached.serial_number_id(), monitor.serial_number_id())
{
attached_snid.eq(m_snid)
} else {
false
};
if serial_number_ids_match || attached.device_id().eq(monitor.device_id()) {
monitor.set_id(attached.id());
monitor.set_device(attached.device().clone());
monitor.set_device_id(attached.device_id().clone());
@@ -380,8 +436,18 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
workspace_rules.remove(i);
}
// Let's add their state to the cache for later
monitor_cache.insert(id, m.clone());
// Let's add their state to the cache for later, make sure to use what
// the user set as preference as the id.
let dip = DISPLAY_INDEX_PREFERENCES.read();
let mut dip_ids = dip.values();
let preferred_id = if dip_ids.any(|id| id == m.device_id()) {
m.device_id().clone()
} else if dip_ids.any(|id| Some(id) == m.serial_number_id().as_ref()) {
m.serial_number_id().clone().unwrap_or_default()
} else {
id
};
monitor_cache.insert(preferred_id, m.clone());
}
}
@@ -487,6 +553,8 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
workspace_names: cached.workspace_names.clone(),
container_padding: cached.container_padding,
workspace_padding: cached.workspace_padding,
wallpaper: cached.wallpaper.clone(),
floating_layer_behaviour: cached.floating_layer_behaviour,
};
let focused_workspace_idx = m.focused_workspace_idx();
@@ -661,3 +729,304 @@ pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::window_manager_event::WindowManagerEvent;
use crossbeam_channel::bounded;
use crossbeam_channel::Sender;
use std::path::PathBuf;
use uuid::Uuid;
use windows::Win32::Devices::Display::DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY;
// NOTE: Using RECT instead of RECT since I get a mismatched type error. Can be updated if
// needed.
use windows::Win32::Foundation::RECT;
// Creating a Mock Display Provider
#[derive(Clone)]
struct MockDevice {
hmonitor: isize,
device_path: String,
device_name: String,
device_description: String,
serial_number_id: Option<String>,
size: RECT,
work_area_size: RECT,
device_key: String,
output_technology: Option<DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY>,
}
impl From<MockDevice> for win32_display_data::Device {
fn from(mock: MockDevice) -> Self {
win32_display_data::Device {
hmonitor: mock.hmonitor,
device_path: mock.device_path,
device_name: mock.device_name,
device_description: mock.device_description,
serial_number_id: mock.serial_number_id,
size: mock.size,
work_area_size: mock.work_area_size,
device_key: mock.device_key,
output_technology: mock.output_technology,
}
}
}
// Creating a Window Manager Instance
struct TestContext {
socket_path: Option<PathBuf>,
}
impl Drop for TestContext {
fn drop(&mut self) {
if let Some(socket_path) = &self.socket_path {
// Clean up the socket file
if let Err(e) = std::fs::remove_file(socket_path) {
tracing::warn!("Failed to remove socket file: {}", e);
}
}
}
}
fn setup_window_manager() -> (WindowManager, TestContext) {
let (_sender, receiver): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
bounded(1);
// Temporary socket path for testing
let socket_name = format!("komorebi-test-{}.sock", Uuid::new_v4());
let socket_path = PathBuf::from(socket_name);
// Create a new WindowManager instance
let wm = match WindowManager::new(receiver, Some(socket_path.clone())) {
Ok(manager) => manager,
Err(e) => {
panic!("Failed to create WindowManager: {}", e);
}
};
(
wm,
TestContext {
socket_path: Some(socket_path),
},
)
}
#[test]
fn test_send_notification() {
// Create a monitor notification
let notification = MonitorNotification::ResolutionScalingChanged;
// Use the send_notification function to send the notification
send_notification(notification);
// Receive the notification from the channel
let received = event_rx().try_recv();
// Check if we received the notification and if it matches what we sent
match received {
Ok(notification) => {
assert_eq!(notification, MonitorNotification::ResolutionScalingChanged);
}
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
}
}
#[test]
fn test_channel_bounded_capacity() {
let (_, receiver) = channel();
// Fill the channel to its capacity (20 messages)
for _ in 0..20 {
send_notification(MonitorNotification::WorkAreaChanged);
}
// Attempt to send another message (should be dropped)
send_notification(MonitorNotification::ResolutionScalingChanged);
// Verify the channel contains only the first 20 messages
for _ in 0..20 {
let notification = match receiver.try_recv() {
Ok(notification) => notification,
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
};
assert_eq!(
notification,
MonitorNotification::WorkAreaChanged,
"Unexpected notification in the channel"
);
}
// Verify that no additional messages are in the channel
assert!(
receiver.try_recv().is_err(),
"Channel should be empty after consuming all messages"
);
}
#[test]
fn test_insert_in_monitor_cache() {
let m = monitor::new(
0,
Rect::default(),
Rect::default(),
"Test Monitor".to_string(),
"Test Device".to_string(),
"Test Device ID".to_string(),
Some("TestMonitorID".to_string()),
);
// Insert the monitor into the cache
insert_in_monitor_cache("TestMonitorID", m.clone());
// Retrieve the monitor from the cache
let cache = MONITOR_CACHE
.get_or_init(|| Mutex::new(HashMap::new()))
.lock();
let retrieved_monitor = cache.get("TestMonitorID");
// Check that the monitor was inserted correctly and matches the expected value
assert_eq!(retrieved_monitor, Some(&m));
}
#[test]
fn test_insert_two_monitors_cache() {
let m1 = monitor::new(
0,
Rect::default(),
Rect::default(),
"Test Monitor".to_string(),
"Test Device".to_string(),
"Test Device ID".to_string(),
Some("TestMonitorID".to_string()),
);
let m2 = monitor::new(
0,
Rect::default(),
Rect::default(),
"Test Monitor 2".to_string(),
"Test Device 2".to_string(),
"Test Device ID 2".to_string(),
Some("TestMonitorID2".to_string()),
);
// Insert the first monitor into the cache
insert_in_monitor_cache("TestMonitorID", m1.clone());
// Insert the second monitor into the cache
insert_in_monitor_cache("TestMonitorID2", m2.clone());
// Retrieve the cache to check if the first and second monitors are present
let cache = MONITOR_CACHE
.get_or_init(|| Mutex::new(HashMap::new()))
.lock();
// Check if Monitor 1 was found in the cache
assert_eq!(
cache.get("TestMonitorID"),
Some(&m1),
"Monitor cache should contain monitor 1"
);
// Check if Monitor 2 was found in the cache
assert_eq!(
cache.get("TestMonitorID2"),
Some(&m2),
"Monitor cache should contain monitor 2"
);
}
#[test]
fn test_listen_for_notifications() {
// Create a WindowManager instance for testing
let (wm, _test_context) = setup_window_manager();
// Start the notification listener
let result = listen_for_notifications(Arc::new(Mutex::new(wm)));
// Check if the listener started successfully
assert!(result.is_ok(), "Failed to start notification listener");
// Test sending a notification
send_notification(MonitorNotification::DisplayConnectionChange);
// Receive the notification from the channel
let received = event_rx().try_recv();
// Check if we received the notification and if it matches what we sent
match received {
Ok(notification) => {
assert_eq!(notification, MonitorNotification::DisplayConnectionChange);
}
Err(e) => panic!("Failed to receive MonitorNotification: {}", e),
}
}
#[test]
fn test_attached_display_devices() {
// Define mock display data
let mock_monitor = MockDevice {
hmonitor: 1,
device_path: String::from(
"\\\\?\\DISPLAY#ABC123#4&123456&0&UID0#{saucepackets-4321-5678-2468-abc123456789}",
),
device_name: String::from("\\\\.\\DISPLAY1"),
device_description: String::from("Display description"),
serial_number_id: Some(String::from("SaucePackets123")),
device_key: String::from("Mock Key"),
size: RECT {
left: 0,
top: 0,
right: 1920,
bottom: 1080,
},
work_area_size: RECT {
left: 0,
top: 0,
right: 1920,
bottom: 1080,
},
output_technology: Some(DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY(0)),
};
// Create a closure to simulate the display provider
let display_provider = || {
vec![Ok::<win32_display_data::Device, win32_display_data::Error>(
win32_display_data::Device::from(mock_monitor.clone()),
)]
.into_iter()
};
// Should contain the mock monitor
let result = attached_display_devices(display_provider).ok();
if let Some(monitors) = result {
// Check Number of monitors
assert_eq!(monitors.len(), 1, "Expected one monitor");
// hmonitor
assert_eq!(monitors[0].id(), 1);
// device name
assert_eq!(monitors[0].name(), &String::from("DISPLAY1"));
// Device
assert_eq!(monitors[0].device(), &String::from("ABC123"));
// Device ID
assert_eq!(
monitors[0].device_id(),
&String::from("ABC123-4&123456&0&UID0")
);
// Check monitor serial number id
assert_eq!(
monitors[0].serial_number_id,
Some(String::from("SaucePackets123")),
);
} else {
panic!("No monitors found");
}
}
}

View File

@@ -1,3 +1,10 @@
use color_eyre::eyre::anyhow;
use color_eyre::eyre::OptionExt;
use color_eyre::Result;
use komorebi_themes::colour::Rgb;
use miow::pipe::connect;
use net2::TcpStreamExt;
use parking_lot::Mutex;
use std::collections::HashMap;
use std::fs::File;
use std::fs::OpenOptions;
@@ -11,20 +18,20 @@ use std::str::FromStr;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
use miow::pipe::connect;
use net2::TcpStreamExt;
use parking_lot::Mutex;
use schemars::gen::SchemaSettings;
use schemars::schema_for;
use uds_windows::UnixStream;
use crate::animation::ANIMATION_DURATION_GLOBAL;
use crate::animation::ANIMATION_DURATION_PER_ANIMATION;
use crate::animation::ANIMATION_ENABLED_GLOBAL;
use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
use crate::animation::ANIMATION_FPS;
use crate::animation::ANIMATION_STYLE_GLOBAL;
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
use crate::core::config_generation::ApplicationConfiguration;
use crate::border_manager;
use crate::border_manager::IMPLEMENTATION;
use crate::border_manager::STYLE;
use crate::build;
use crate::config_generation::WorkspaceMatchingRule;
use crate::core::config_generation::IdWithIdentifier;
use crate::core::config_generation::MatchingRule;
use crate::core::config_generation::MatchingStrategy;
@@ -41,16 +48,6 @@ use crate::core::SocketMessage;
use crate::core::StateQuery;
use crate::core::WindowContainerBehaviour;
use crate::core::WindowKind;
use crate::animation::ANIMATION_DURATION_GLOBAL;
use crate::animation::ANIMATION_ENABLED_GLOBAL;
use crate::animation::ANIMATION_FPS;
use crate::animation::ANIMATION_STYLE_GLOBAL;
use crate::border_manager;
use crate::border_manager::IMPLEMENTATION;
use crate::border_manager::STYLE;
use crate::colour::Rgb;
use crate::config_generation::WorkspaceMatchingRule;
use crate::current_virtual_desktop;
use crate::monitor::MonitorInformation;
use crate::notify_subscribers;
@@ -75,6 +72,7 @@ use crate::State;
use crate::CUSTOM_FFM;
use crate::DATA_DIR;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::FLOATING_APPLICATIONS;
use crate::HIDING_BEHAVIOUR;
use crate::IGNORE_IDENTIFIERS;
use crate::INITIAL_CONFIGURATION_LOADED;
@@ -84,6 +82,7 @@ use crate::MONITOR_INDEX_PREFERENCES;
use crate::NO_TITLEBAR;
use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::REMOVE_TITLEBARS;
use crate::SESSION_FLOATING_APPLICATIONS;
use crate::SUBSCRIPTION_PIPES;
use crate::SUBSCRIPTION_SOCKETS;
use crate::SUBSCRIPTION_SOCKET_OPTIONS;
@@ -209,7 +208,9 @@ impl WindowManager {
let initial_state = State::from(self.as_ref());
match message {
SocketMessage::CycleFocusWorkspace(_) | SocketMessage::FocusWorkspaceNumber(_) => {
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));
@@ -225,6 +226,7 @@ impl WindowManager {
_ => {}
};
let mut force_update_borders = false;
match message {
SocketMessage::Promote => self.promote_container_to_front()?,
SocketMessage::PromoteFocus => self.promote_focus_to_front()?,
@@ -252,9 +254,11 @@ impl WindowManager {
if let Some((monitor_idx, workspace_idx)) = monitor_workspace_indices {
if monitor_idx != focused_monitor_idx {
self.focus_monitor(monitor_idx)?;
}
if workspace_idx != focused_workspace_idx {
let focused_ws_idx = self.focused_workspace_idx()?;
if focused_ws_idx != workspace_idx {
self.focus_workspace(workspace_idx)?;
}
} else if workspace_idx != focused_workspace_idx {
self.focus_workspace(workspace_idx)?;
}
}
@@ -333,11 +337,9 @@ impl WindowManager {
SocketMessage::UnstackAll => self.unstack_all()?,
SocketMessage::CycleStack(direction) => {
self.cycle_container_window_in_direction(direction)?;
self.focused_window()?.focus(self.mouse_follows_focus)?;
}
SocketMessage::CycleStackIndex(direction) => {
self.cycle_container_window_index_in_direction(direction)?;
self.focused_window()?.focus(self.mouse_follows_focus)?;
}
SocketMessage::FocusStackWindow(idx) => {
// In case you are using this command on a bar on a monitor
@@ -348,7 +350,6 @@ impl WindowManager {
self.focus_monitor(monitor_idx)?;
}
self.focus_container_window(idx)?;
self.focused_window()?.focus(self.mouse_follows_focus)?;
}
SocketMessage::ForceFocus => {
let focused_window = self.focused_window()?;
@@ -362,7 +363,42 @@ impl WindowManager {
SocketMessage::Minimize => {
Window::from(WindowsApi::foreground_window()?).minimize();
}
SocketMessage::ToggleFloat => self.toggle_float()?,
SocketMessage::LockMonitorWorkspaceContainer(
monitor_idx,
workspace_idx,
container_idx,
) => {
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.ok_or_eyre("no monitor at the given index")?;
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.ok_or_eyre("no workspace at the given index")?;
workspace.locked_containers.insert(container_idx);
}
SocketMessage::UnlockMonitorWorkspaceContainer(
monitor_idx,
workspace_idx,
container_idx,
) => {
let monitor = self
.monitors_mut()
.get_mut(monitor_idx)
.ok_or_eyre("no monitor at the given index")?;
let workspace = monitor
.workspaces_mut()
.get_mut(workspace_idx)
.ok_or_eyre("no workspace at the given index")?;
workspace.locked_containers.remove(&container_idx);
}
SocketMessage::ToggleLock => self.toggle_lock()?,
SocketMessage::ToggleFloat => self.toggle_float(false)?,
SocketMessage::ToggleMonocle => self.toggle_monocle()?,
SocketMessage::ToggleMaximize => self.toggle_maximize()?,
SocketMessage::ContainerPadding(monitor_idx, workspace_idx, size) => {
@@ -509,6 +545,53 @@ impl WindowManager {
}));
}
}
SocketMessage::SessionFloatRule => {
let foreground_window = WindowsApi::foreground_window()?;
let window = Window::from(foreground_window);
if let (Ok(exe), Ok(title), Ok(class)) =
(window.exe(), window.title(), window.class())
{
let rule = MatchingRule::Composite(vec![
IdWithIdentifier {
kind: ApplicationIdentifier::Exe,
id: exe,
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
kind: ApplicationIdentifier::Title,
id: title,
matching_strategy: Option::from(MatchingStrategy::Equals),
},
IdWithIdentifier {
kind: ApplicationIdentifier::Class,
id: class,
matching_strategy: Option::from(MatchingStrategy::Equals),
},
]);
let mut floating_applications = FLOATING_APPLICATIONS.lock();
floating_applications.push(rule.clone());
let mut session_floating_applications = SESSION_FLOATING_APPLICATIONS.lock();
session_floating_applications.push(rule.clone());
self.toggle_float(true)?;
}
}
SocketMessage::SessionFloatRules => {
let session_floating_applications = SESSION_FLOATING_APPLICATIONS.lock();
let rules = match serde_json::to_string_pretty(&*session_floating_applications) {
Ok(rules) => rules,
Err(error) => error.to_string(),
};
reply.write_all(rules.as_bytes())?;
}
SocketMessage::ClearSessionFloatRules => {
let mut floating_applications = FLOATING_APPLICATIONS.lock();
let mut session_floating_applications = SESSION_FLOATING_APPLICATIONS.lock();
floating_applications.retain(|r| !session_floating_applications.contains(r));
session_floating_applications.clear()
}
SocketMessage::IgnoreRule(identifier, ref id) => {
let mut ignore_identifiers = IGNORE_IDENTIFIERS.lock();
@@ -607,6 +690,67 @@ impl WindowManager {
SocketMessage::AdjustWorkspacePadding(sizing, adjustment) => {
self.adjust_workspace_padding(sizing, adjustment)?;
}
SocketMessage::MoveContainerToLastWorkspace => {
// This is to ensure that even on an empty workspace on a secondary monitor, the
// secondary monitor where the cursor is focused will be used as the target for
// the workspace switch op
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
if monitor_idx != self.focused_monitor_idx() {
if let Some(monitor) = self.monitors().get(monitor_idx) {
if let Some(workspace) = monitor.focused_workspace() {
if workspace.is_empty() {
self.focus_monitor(monitor_idx)?;
}
}
}
}
}
let idx = self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?
.focused_workspace_idx();
if let Some(monitor) = self.focused_monitor_mut() {
if let Some(last_focused_workspace) = monitor.last_focused_workspace() {
self.move_container_to_workspace(last_focused_workspace, true, None)?;
}
}
self.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no monitor"))?
.set_last_focused_workspace(Option::from(idx));
}
SocketMessage::SendContainerToLastWorkspace => {
// This is to ensure that even on an empty workspace on a secondary monitor, the
// secondary monitor where the cursor is focused will be used as the target for
// the workspace switch op
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
if monitor_idx != self.focused_monitor_idx() {
if let Some(monitor) = self.monitors().get(monitor_idx) {
if let Some(workspace) = monitor.focused_workspace() {
if workspace.is_empty() {
self.focus_monitor(monitor_idx)?;
}
}
}
}
}
let idx = self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?
.focused_workspace_idx();
if let Some(monitor) = self.focused_monitor_mut() {
if let Some(last_focused_workspace) = monitor.last_focused_workspace() {
self.move_container_to_workspace(last_focused_workspace, false, None)?;
}
}
self.focused_monitor_mut()
.ok_or_else(|| anyhow!("there is no monitor"))?
.set_last_focused_workspace(Option::from(idx));
}
SocketMessage::MoveContainerToWorkspaceNumber(workspace_idx) => {
self.move_container_to_workspace(workspace_idx, true, None)?;
}
@@ -767,10 +911,12 @@ impl WindowManager {
}
SocketMessage::Retile => {
border_manager::destroy_all_borders()?;
force_update_borders = true;
self.retile_all(false)?
}
SocketMessage::RetileWithResizeDimensions => {
border_manager::destroy_all_borders()?;
force_update_borders = true;
self.retile_all(true)?
}
SocketMessage::FlipLayout(layout_flip) => self.flip_layout(layout_flip)?,
@@ -904,6 +1050,55 @@ impl WindowManager {
self.focus_workspace(workspace_idx)?;
}
SocketMessage::CycleFocusEmptyWorkspace(direction) => {
// This is to ensure that even on an empty workspace on a secondary monitor, the
// secondary monitor where the cursor is focused will be used as the target for
// the workspace switch op
if let Some(monitor_idx) = self.monitor_idx_from_current_pos() {
if monitor_idx != self.focused_monitor_idx() {
if let Some(monitor) = self.monitors().get(monitor_idx) {
if let Some(workspace) = monitor.focused_workspace() {
if workspace.is_empty() {
self.focus_monitor(monitor_idx)?;
}
}
}
}
}
let focused_monitor = self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?;
let focused_workspace_idx = focused_monitor.focused_workspace_idx();
let workspaces = focused_monitor.workspaces().len();
let mut empty_workspaces = vec![];
for (idx, w) in focused_monitor.workspaces().iter().enumerate() {
if w.is_empty() {
empty_workspaces.push(idx);
}
}
if !empty_workspaces.is_empty() {
let mut workspace_idx = direction.next_idx(
focused_workspace_idx,
NonZeroUsize::new(workspaces)
.ok_or_else(|| anyhow!("there must be at least one workspace"))?,
);
while !empty_workspaces.contains(&workspace_idx) {
workspace_idx = direction.next_idx(
workspace_idx,
NonZeroUsize::new(workspaces)
.ok_or_else(|| anyhow!("there must be at least one workspace"))?,
);
}
self.focus_workspace(workspace_idx)?;
}
}
SocketMessage::CloseWorkspace => {
// This is to ensure that even on an empty workspace on a secondary monitor, the
// secondary monitor where the cursor is focused will be used as the target for
@@ -1054,11 +1249,33 @@ impl WindowManager {
WorkspaceLayer::Tiling => {
workspace.set_layer(WorkspaceLayer::Floating);
for (i, window) in workspace.floating_windows().iter().enumerate() {
if i == 0 {
let focused_idx = workspace.focused_floating_window_idx();
let mut window_idx_pairs = workspace
.floating_windows_mut()
.make_contiguous()
.iter()
.enumerate()
.collect::<Vec<_>>();
// Sort by window area
window_idx_pairs.sort_by_key(|(_, w)| {
let rect = WindowsApi::window_rect(w.hwnd).unwrap_or_default();
rect.right * rect.bottom
});
window_idx_pairs.reverse();
for (i, window) in window_idx_pairs {
if i == focused_idx {
to_focus = Some(*window);
} else {
window.raise()?;
}
window.raise()?;
}
if let Some(focused_window) = &to_focus {
// The focused window should be the last one raised to make sure it is
// on top
focused_window.raise()?;
}
for container in workspace.containers() {
@@ -1080,7 +1297,19 @@ impl WindowManager {
}
}
for window in workspace.floating_windows() {
let mut window_idx_pairs = workspace
.floating_windows_mut()
.make_contiguous()
.iter()
.collect::<Vec<_>>();
// Sort by window area
window_idx_pairs.sort_by_key(|w| {
let rect = WindowsApi::window_rect(w.hwnd).unwrap_or_default();
rect.right * rect.bottom
});
for window in window_idx_pairs {
window.lower()?;
}
}
@@ -1108,7 +1337,7 @@ impl WindowManager {
);
}
SocketMessage::DisplayIndexPreference(index_preference, ref display) => {
let mut display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
let mut display_index_preferences = DISPLAY_INDEX_PREFERENCES.write();
display_index_preferences.insert(index_preference, display.clone());
}
SocketMessage::EnsureWorkspaces(monitor_idx, workspace_count) => {
@@ -1200,6 +1429,20 @@ impl WindowManager {
.focused_workspace_name()
.unwrap_or_else(|| focused_monitor.focused_workspace_idx().to_string())
}
StateQuery::Version => build::RUST_VERSION.to_string(),
StateQuery::FocusedWorkspaceLayout => {
let focused_monitor = self
.focused_monitor()
.ok_or_else(|| anyhow!("there is no monitor"))?;
focused_monitor.focused_workspace_layout().map_or_else(
|| "None".to_string(),
|layout| match layout {
Layout::Default(default_layout) => default_layout.to_string(),
Layout::Custom(_) => "Custom".to_string(),
},
)
}
};
reply.write_all(response.as_bytes())?;
@@ -1409,6 +1652,7 @@ impl WindowManager {
}
SocketMessage::ReloadConfiguration => {
Self::reload_configuration();
force_update_borders = true;
}
SocketMessage::ReplaceConfiguration(ref config) => {
// Check that this is a valid static config file first
@@ -1437,15 +1681,78 @@ impl WindowManager {
// Set self to the new wm instance
*self = wm;
// check if there are any bars
let mut system = sysinfo::System::new_all();
system.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
let has_bar = system
.processes_by_name("komorebi-bar.exe".as_ref())
.next()
.is_some();
// stop bar(s)
if has_bar {
let script = r"
Stop-Process -Name:komorebi-bar -ErrorAction SilentlyContinue
";
match powershell_script::run(script) {
Ok(_) => {
println!("{script}");
// start new bar(s)
let mut config = StaticConfig::read(config)?;
if let Some(display_bar_configurations) =
&mut config.bar_configurations
{
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());
match powershell_script::run(&script) {
Ok(_) => {
println!("{script}");
}
Err(error) => {
println!("Error: {error}");
}
}
}
} else {
let script = r"
if (!(Get-Process komorebi-bar -ErrorAction SilentlyContinue))
{
Start-Process komorebi-bar -WindowStyle hidden
}
";
match powershell_script::run(script) {
Ok(_) => {
println!("{script}");
}
Err(error) => {
println!("Error: {error}");
}
}
}
}
Err(error) => {
println!("Error: {error}");
}
}
}
force_update_borders = true;
}
}
SocketMessage::ReloadStaticConfiguration(ref pathbuf) => {
self.reload_static_configuration(pathbuf)?;
force_update_borders = true;
}
SocketMessage::CompleteConfiguration => {
if !INITIAL_CONFIGURATION_LOADED.load(Ordering::SeqCst) {
INITIAL_CONFIGURATION_LOADED.store(true, Ordering::SeqCst);
self.update_focused_workspace(false, false)?;
force_update_borders = true;
}
}
SocketMessage::WatchConfiguration(enable) => {
@@ -1703,6 +2010,8 @@ impl WindowManager {
self.remove_all_accents()?;
}
}
} else if matches!(IMPLEMENTATION.load(), BorderImplementation::Komorebi) {
force_update_borders = true;
}
}
SocketMessage::BorderImplementation(implementation) => {
@@ -1715,40 +2024,49 @@ impl WindowManager {
match IMPLEMENTATION.load() {
BorderImplementation::Komorebi => {
self.remove_all_accents()?;
force_update_borders = true;
}
BorderImplementation::Windows => {
border_manager::destroy_all_borders()?;
}
}
border_manager::send_notification(None);
}
}
SocketMessage::BorderColour(kind, r, g, b) => match kind {
WindowKind::Single => {
border_manager::FOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
SocketMessage::BorderColour(kind, r, g, b) => {
match kind {
WindowKind::Single => {
border_manager::FOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowKind::Stack => {
border_manager::STACK.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowKind::Monocle => {
border_manager::MONOCLE.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowKind::Unfocused => {
border_manager::UNFOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowKind::UnfocusedLocked => {
border_manager::UNFOCUSED_LOCKED
.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowKind::Floating => {
border_manager::FLOATING.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
}
WindowKind::Stack => {
border_manager::STACK.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowKind::Monocle => {
border_manager::MONOCLE.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowKind::Unfocused => {
border_manager::UNFOCUSED.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
WindowKind::Floating => {
border_manager::FLOATING.store(Rgb::new(r, g, b).into(), Ordering::SeqCst);
}
},
force_update_borders = true;
}
SocketMessage::BorderStyle(style) => {
STYLE.store(style);
force_update_borders = true;
}
SocketMessage::BorderWidth(width) => {
border_manager::BORDER_WIDTH.store(width, Ordering::SeqCst);
force_update_borders = true;
}
SocketMessage::BorderOffset(offset) => {
border_manager::BORDER_OFFSET.store(offset, Ordering::SeqCst);
force_update_borders = true;
}
SocketMessage::Animation(enable, prefix) => match prefix {
Some(prefix) => {
@@ -1828,35 +2146,49 @@ impl WindowManager {
*STACKBAR_FONT_FAMILY.lock() = font_family.clone();
}
SocketMessage::ApplicationSpecificConfigurationSchema => {
let asc = schema_for!(Vec<ApplicationConfiguration>);
let schema = serde_json::to_string_pretty(&asc)?;
#[cfg(feature = "schemars")]
{
let asc = schemars::schema_for!(
Vec<crate::core::config_generation::ApplicationConfiguration>
);
let schema = serde_json::to_string_pretty(&asc)?;
reply.write_all(schema.as_bytes())?;
reply.write_all(schema.as_bytes())?;
}
}
SocketMessage::NotificationSchema => {
let notification = schema_for!(Notification);
let schema = serde_json::to_string_pretty(&notification)?;
#[cfg(feature = "schemars")]
{
let notification = schemars::schema_for!(Notification);
let schema = serde_json::to_string_pretty(&notification)?;
reply.write_all(schema.as_bytes())?;
reply.write_all(schema.as_bytes())?;
}
}
SocketMessage::SocketSchema => {
let socket_message = schema_for!(SocketMessage);
let schema = serde_json::to_string_pretty(&socket_message)?;
#[cfg(feature = "schemars")]
{
let socket_message = schemars::schema_for!(SocketMessage);
let schema = serde_json::to_string_pretty(&socket_message)?;
reply.write_all(schema.as_bytes())?;
reply.write_all(schema.as_bytes())?;
}
}
SocketMessage::StaticConfigSchema => {
let settings = SchemaSettings::default().with(|s| {
s.option_nullable = false;
s.option_add_null_type = false;
s.inline_subschemas = true;
});
#[cfg(feature = "schemars")]
{
let settings = schemars::gen::SchemaSettings::default().with(|s| {
s.option_nullable = false;
s.option_add_null_type = false;
s.inline_subschemas = true;
});
let gen = settings.into_generator();
let socket_message = gen.into_root_schema_for::<StaticConfig>();
let schema = serde_json::to_string_pretty(&socket_message)?;
let gen = settings.into_generator();
let socket_message = gen.into_root_schema_for::<StaticConfig>();
let schema = serde_json::to_string_pretty(&socket_message)?;
reply.write_all(schema.as_bytes())?;
reply.write_all(schema.as_bytes())?;
}
}
SocketMessage::GenerateStaticConfig => {
let config = serde_json::to_string_pretty(&StaticConfig::from(&*self))?;
@@ -1896,8 +2228,8 @@ impl WindowManager {
reply.write_all(schema.as_bytes())?;
}
SocketMessage::Theme(theme) => {
theme_manager::send_notification(theme);
SocketMessage::Theme(ref theme) => {
theme_manager::send_notification(*theme.clone());
}
// Deprecated commands
SocketMessage::AltFocusHack(_)
@@ -1915,7 +2247,11 @@ impl WindowManager {
initial_state.has_been_modified(self.as_ref()),
)?;
border_manager::send_notification(None);
if force_update_borders {
border_manager::send_force_update();
} else {
border_manager::send_notification(None);
}
transparency_manager::send_notification();
stackbar_manager::send_notification();
@@ -2008,3 +2344,73 @@ pub fn read_commands_tcp(
Ok(())
}
#[cfg(test)]
mod tests {
use crate::monitor;
use crate::window_manager::WindowManager;
use crate::Rect;
use crate::SocketMessage;
use crate::WindowManagerEvent;
use crossbeam_channel::bounded;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Write;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;
use uds_windows::UnixStream;
use uuid::Uuid;
fn send_socket_message(socket: &PathBuf, message: SocketMessage) {
let mut stream = UnixStream::connect(socket).unwrap();
stream
.set_write_timeout(Some(Duration::from_secs(1)))
.unwrap();
stream
.write_all(serde_json::to_string(&message).unwrap().as_bytes())
.unwrap();
}
#[test]
fn test_receive_socket_message() {
let (_sender, receiver): (Sender<WindowManagerEvent>, Receiver<WindowManagerEvent>) =
bounded(1);
let socket_name = format!("komorebi-test-{}.sock", Uuid::new_v4());
let socket_path = PathBuf::from(&socket_name);
let mut wm = WindowManager::new(receiver, Some(socket_path.clone())).unwrap();
let m = monitor::new(
0,
Rect::default(),
Rect::default(),
"TestMonitor".to_string(),
"TestDevice".to_string(),
"TestDeviceID".to_string(),
Some("TestMonitorID".to_string()),
);
wm.monitors_mut().push_back(m);
// send a message
send_socket_message(&socket_path, SocketMessage::FocusWorkspaceNumber(5));
let (stream, _) = wm.command_listener.accept().unwrap();
let reader = BufReader::new(stream.try_clone().unwrap());
let next = reader.lines().next();
// read and deserialize the message
let message_string = next.unwrap().unwrap();
let message = SocketMessage::from_str(&message_string).unwrap();
assert!(matches!(message, SocketMessage::FocusWorkspaceNumber(5)));
// process the message
wm.process_command(message, stream).unwrap();
// check the updated window manager state
assert_eq!(wm.focused_workspace_idx().unwrap(), 5);
std::fs::remove_file(socket_path).unwrap();
}
}

View File

@@ -1,7 +1,5 @@
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;
use color_eyre::eyre::anyhow;
use color_eyre::Result;
@@ -27,12 +25,10 @@ use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::winevent::WinEvent;
use crate::workspace::WorkspaceLayer;
use crate::workspace_reconciliator;
use crate::workspace_reconciliator::ALT_TAB_HWND;
use crate::workspace_reconciliator::ALT_TAB_HWND_INSTANT;
use crate::Notification;
use crate::NotificationEvent;
use crate::State;
use crate::Window;
use crate::FLOATING_APPLICATIONS;
use crate::HIDDEN_HWNDS;
use crate::REGEX_IDENTIFIERS;
@@ -304,34 +300,7 @@ impl WindowManager {
let focused_workspace_idx =
self.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
let mut needs_reconciliation = false;
if let Some((m_idx, w_idx)) = self.known_hwnds.get(&window.hwnd) {
if focused_pair != (*m_idx, *w_idx) {
// At this point we know we are going to send a notification to the workspace reconciliator
// So we get the topmost window returned by EnumWindows, which is almost always the window
// that has been selected by alt-tab
if let Ok(alt_tab_windows) = WindowsApi::alt_tab_windows() {
if let Some(first) =
alt_tab_windows.iter().find(|w| w.title().is_ok())
{
// If our record of this HWND hasn't been updated in over a minute
let mut instant = ALT_TAB_HWND_INSTANT.lock();
if instant.elapsed().gt(&Duration::from_secs(1)) {
// Update our record with the HWND we just found
ALT_TAB_HWND.store(Some(first.hwnd));
// Update the timestamp of our record
*instant = Instant::now();
}
}
}
workspace_reconciliator::send_notification(*m_idx, *w_idx);
needs_reconciliation = true;
}
}
let mut needs_reconciliation = None;
// There are some applications such as Firefox where, if they are focused when a
// workspace switch takes place, it will fire an additional Show event, which will
@@ -340,6 +309,23 @@ impl WindowManager {
// duplicates across multiple workspaces, as it results in ghost layout tiles.
let mut proceed = true;
// Check for potential `alt-tab` event
if matches!(
event,
WindowManagerEvent::Uncloak(_, _) | WindowManagerEvent::Show(_, _)
) {
needs_reconciliation = self.needs_reconciliation(window)?;
if let Some((m_idx, ws_idx)) = needs_reconciliation {
self.perform_reconciliation(window, (m_idx, ws_idx))?;
// Since there was a reconciliation after an `alt-tab`, that means this
// window is already handled by komorebi so we shouldn't proceed with
// adding it as a new window.
proceed = false;
}
}
if let Some((m_idx, w_idx)) = self.known_hwnds.get(&window.hwnd) {
if let Some(focused_workspace_idx) = self
.monitors()
@@ -368,7 +354,7 @@ impl WindowManager {
let workspace_contains_window = workspace.contains_window(window.hwnd);
let monocle_container = workspace.monocle_container().clone();
if !workspace_contains_window && !needs_reconciliation {
if !workspace_contains_window && needs_reconciliation.is_none() {
let floating_applications = FLOATING_APPLICATIONS.lock();
let mut should_float = false;
@@ -395,8 +381,19 @@ impl WindowManager {
&& !matches!(event, WindowManagerEvent::Manage(_)));
if behaviour.float_override {
workspace.floating_windows_mut().push(window);
// Center floating windows if we are already on the `Floating`
// layer and the window doesn't match a `floating_windows` rule and
// the workspace is not a floating workspace
let center_spawned_floats =
matches!(workspace.layer, WorkspaceLayer::Floating)
&& !should_float
&& workspace.tile;
workspace.floating_windows_mut().push_back(window);
workspace.set_layer(WorkspaceLayer::Floating);
if center_spawned_floats {
let mut floating_window = window;
floating_window.center(&workspace.globals().work_area)?;
}
self.update_focused_workspace(false, false)?;
} else {
match behaviour.current_behaviour {
@@ -619,7 +616,7 @@ impl WindowManager {
window.focus(self.mouse_follows_focus)?;
}
} else if window_management_behaviour.float_override {
workspace.floating_windows_mut().push(window);
workspace.floating_windows_mut().push_back(window);
self.update_focused_workspace(false, false)?;
} else {
match window_management_behaviour.current_behaviour {
@@ -742,4 +739,119 @@ impl WindowManager {
Ok(())
}
/// Checks if this window is from another unfocused workspace or is an unfocused window on a
/// stack container. If it is it will return the monitor/workspace index pair of this window so
/// that a reconciliation of that monitor/workspace can be done.
fn needs_reconciliation(&self, window: Window) -> color_eyre::Result<Option<(usize, usize)>> {
let focused_monitor_idx = self.focused_monitor_idx();
let focused_workspace_idx =
self.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
let mut needs_reconciliation = None;
if let Some((m_idx, ws_idx)) = self.known_hwnds.get(&window.hwnd) {
if (*m_idx, *ws_idx) == focused_pair {
if let Some(target_workspace) = self
.monitors()
.get(*m_idx)
.and_then(|m| m.workspaces().get(*ws_idx))
{
if let Some(monocle_with_window) = target_workspace
.monocle_container()
.as_ref()
.and_then(|m| m.contains_window(window.hwnd).then_some(m))
{
if monocle_with_window.focused_window() != Some(&window) {
tracing::debug!("Needs reconciliation within a monocled stack");
needs_reconciliation = Some((*m_idx, *ws_idx));
}
} else {
let c_idx = target_workspace.container_idx_for_window(window.hwnd);
if let Some(target_container) =
c_idx.and_then(|c_idx| target_workspace.containers().get(c_idx))
{
if target_container.focused_window() != Some(&window) {
tracing::debug!(
"Needs reconciliation within a stack on the focused workspace"
);
needs_reconciliation = Some((*m_idx, *ws_idx));
}
}
}
}
} else {
tracing::debug!("Needs reconciliation for a different monitor/workspace pair");
needs_reconciliation = Some((*m_idx, *ws_idx));
}
}
Ok(needs_reconciliation)
}
/// When there was an `alt-tab` to a hidden window we need to perform a reconciliation, meaning
/// we need to update the focused monitor, workspace, container and window indices to the ones
/// corresponding to the window the user just alt-tabbed into.
fn perform_reconciliation(
&mut self,
window: Window,
reconciliation_pair: (usize, usize),
) -> color_eyre::Result<()> {
let (m_idx, ws_idx) = reconciliation_pair;
tracing::debug!("performing reconciliation");
self.focus_monitor(m_idx)?;
let mouse_follows_focus = self.mouse_follows_focus;
let offset = self.work_area_offset;
if let Some(monitor) = self.focused_monitor_mut() {
if ws_idx != monitor.focused_workspace_idx() {
let previous_idx = monitor.focused_workspace_idx();
monitor.set_last_focused_workspace(Option::from(previous_idx));
monitor.focus_workspace(ws_idx)?;
}
if let Some(workspace) = monitor.focused_workspace_mut() {
let mut layer = WorkspaceLayer::Tiling;
if let Some((monocle, idx)) = workspace
.monocle_container_mut()
.as_mut()
.and_then(|m| m.idx_for_window(window.hwnd).map(|i| (m, i)))
{
monocle.focus_window(idx);
} else if workspace
.floating_windows()
.iter()
.any(|w| w.hwnd == window.hwnd)
{
layer = WorkspaceLayer::Floating;
} else if !workspace
.maximized_window()
.is_some_and(|w| w.hwnd == window.hwnd)
{
// If the window is the maximized window do nothing, else we
// reintegrate the monocle if it exists and then focus the
// container
if workspace.monocle_container().is_some() {
tracing::info!("disabling monocle");
for container in workspace.containers_mut() {
container.restore();
}
for window in workspace.floating_windows_mut() {
window.restore();
}
workspace.reintegrate_monocle_container()?;
}
workspace.focus_container_by_window(window.hwnd)?;
}
workspace.set_layer(layer);
}
monitor.load_focused_workspace(mouse_follows_focus)?;
monitor.update_focused_workspace(offset)?;
}
Ok(())
}
}

View File

@@ -1,10 +1,10 @@
use std::collections::VecDeque;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Ring<T> {
elements: VecDeque<T>,
focused: usize,
@@ -76,4 +76,36 @@ macro_rules! impl_ring_elements {
}
}
};
// This allows passing a different name to be used for the functions. For instance, the
// `floating_windows` ring calls this as:
// ```rust
// impl_ring_elements!(Workspace, Window, "floating_window");
// ```
// Which allows using the `Window` element but name the functions as `floating_window`
($name:ty, $element:ident, $el_name:literal) => {
paste::paste! {
impl $name {
pub const fn [<$el_name:lower s>](&self) -> &VecDeque<$element> {
self.[<$el_name:lower s>].elements()
}
pub fn [<$el_name:lower s_mut>](&mut self) -> &mut VecDeque<$element> {
self.[<$el_name:lower s>].elements_mut()
}
#[allow(dead_code)]
pub fn [<focused_ $el_name:lower>](&self) -> Option<&$element> {
self.[<$el_name:lower s>].focused()
}
pub const fn [<focused_ $el_name:lower _idx>](&self) -> usize {
self.[<$el_name:lower s>].focused_idx()
}
pub fn [<focused_ $el_name:lower _mut>](&mut self) -> Option<&mut $element> {
self.[<$el_name:lower s>].focused_mut()
}
}
}
};
}

View File

@@ -350,7 +350,7 @@ impl Stackbar {
}
// Restore the window corresponding to the tab we have clicked
window.restore();
window.restore_with_border(false);
if let Err(err) = window.focus(false) {
tracing::error!(
"stackbar WMLBUTTONDOWN focus error: hwnd {} ({})",
@@ -361,7 +361,7 @@ impl Stackbar {
} else {
// Hide any windows in the stack that don't correspond to the window
// we have clicked
window.hide();
window.hide_with_border(false);
}
}
}

View File

@@ -7,18 +7,38 @@ use crate::animation::ANIMATION_FPS;
use crate::animation::ANIMATION_STYLE_GLOBAL;
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
use crate::animation::DEFAULT_ANIMATION_FPS;
use crate::asc::ApplicationSpecificConfiguration;
use crate::asc::AscApplicationRulesOrSchema;
use crate::border_manager;
use crate::border_manager::ZOrder;
use crate::border_manager::IMPLEMENTATION;
use crate::border_manager::STYLE;
use crate::colour::Colour;
use crate::config_generation::WorkspaceMatchingRule;
use crate::core::config_generation::ApplicationConfiguration;
use crate::core::config_generation::ApplicationConfigurationGenerator;
use crate::core::config_generation::ApplicationOptions;
use crate::core::config_generation::MatchingRule;
use crate::core::config_generation::MatchingStrategy;
use crate::core::AnimationStyle;
use crate::core::BorderImplementation;
use crate::core::BorderStyle;
use crate::core::DefaultLayout;
use crate::core::FocusFollowsMouseImplementation;
use crate::core::HidingBehaviour;
use crate::core::Layout;
use crate::core::MoveBehaviour;
use crate::core::OperationBehaviour;
use crate::core::Rect;
use crate::core::SocketMessage;
use crate::core::StackbarLabel;
use crate::core::StackbarMode;
use crate::core::WindowContainerBehaviour;
use crate::core::WindowManagementBehaviour;
use crate::current_virtual_desktop;
use crate::monitor;
use crate::monitor::Monitor;
use crate::monitor_reconciliator;
use crate::resolve_option_hashmap_usize_path;
use crate::ring::Ring;
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::stackbar_manager::STACKBAR_FONT_FAMILY;
@@ -39,7 +59,9 @@ use crate::workspace::Workspace;
use crate::AspectRatio;
use crate::Axis;
use crate::CrossBoundaryBehaviour;
use crate::FloatingLayerBehaviour;
use crate::PredefinedAspectRatio;
use crate::ResolvedPathBuf;
use crate::DATA_DIR;
use crate::DEFAULT_CONTAINER_PADDING;
use crate::DEFAULT_WORKSPACE_PADDING;
@@ -61,35 +83,13 @@ use crate::TRANSPARENCY_BLACKLIST;
use crate::TRAY_AND_MULTI_WINDOW_IDENTIFIERS;
use crate::WINDOWS_11;
use crate::WORKSPACE_MATCHING_RULES;
use crate::asc::ApplicationSpecificConfiguration;
use crate::asc::AscApplicationRulesOrSchema;
use crate::config_generation::WorkspaceMatchingRule;
use crate::core::config_generation::ApplicationConfiguration;
use crate::core::config_generation::ApplicationConfigurationGenerator;
use crate::core::config_generation::ApplicationOptions;
use crate::core::config_generation::MatchingRule;
use crate::core::config_generation::MatchingStrategy;
use crate::core::resolve_home_path;
use crate::core::AnimationStyle;
use crate::core::BorderStyle;
use crate::core::DefaultLayout;
use crate::core::FocusFollowsMouseImplementation;
use crate::core::HidingBehaviour;
use crate::core::Layout;
use crate::core::MoveBehaviour;
use crate::core::OperationBehaviour;
use crate::core::Rect;
use crate::core::SocketMessage;
use crate::core::WindowContainerBehaviour;
use crate::core::WindowManagementBehaviour;
use color_eyre::Result;
use crossbeam_channel::Receiver;
use hotwatch::EventKind;
use hotwatch::Hotwatch;
use komorebi_themes::colour::Colour;
use parking_lot::Mutex;
use regex::Regex;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
@@ -102,7 +102,8 @@ use std::sync::Arc;
use uds_windows::UnixListener;
use uds_windows::UnixStream;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct BorderColours {
/// Border colour when the container contains a single window
#[serde(skip_serializing_if = "Option::is_none")]
@@ -119,9 +120,68 @@ pub struct BorderColours {
/// Border colour when the container is unfocused
#[serde(skip_serializing_if = "Option::is_none")]
pub unfocused: Option<Colour>,
/// Border colour when the container is unfocused and locked
#[serde(skip_serializing_if = "Option::is_none")]
pub unfocused_locked: Option<Colour>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ThemeOptions {
/// Specify Light or Dark variant for theme generation (default: Dark)
#[serde(skip_serializing_if = "Option::is_none")]
pub theme_variant: Option<komorebi_themes::ThemeVariant>,
/// Border colour when the container contains a single window (default: Base0D)
#[serde(skip_serializing_if = "Option::is_none")]
pub single_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container contains multiple windows (default: Base0B)
#[serde(skip_serializing_if = "Option::is_none")]
pub stack_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is in monocle mode (default: Base0F)
#[serde(skip_serializing_if = "Option::is_none")]
pub monocle_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the window is floating (default: Base09)
#[serde(skip_serializing_if = "Option::is_none")]
pub floating_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is unfocused (default: Base01)
#[serde(skip_serializing_if = "Option::is_none")]
pub unfocused_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is unfocused and locked (default: Base08)
#[serde(skip_serializing_if = "Option::is_none")]
pub unfocused_locked_border: Option<komorebi_themes::Base16Value>,
/// Stackbar focused tab text colour (default: Base0B)
#[serde(skip_serializing_if = "Option::is_none")]
pub stackbar_focused_text: Option<komorebi_themes::Base16Value>,
/// Stackbar unfocused tab text colour (default: Base05)
#[serde(skip_serializing_if = "Option::is_none")]
pub stackbar_unfocused_text: Option<komorebi_themes::Base16Value>,
/// Stackbar tab background colour (default: Base01)
#[serde(skip_serializing_if = "Option::is_none")]
pub stackbar_background: Option<komorebi_themes::Base16Value>,
/// Komorebi status bar accent (default: Base0D)
#[serde(skip_serializing_if = "Option::is_none")]
pub bar_accent: Option<komorebi_themes::Base16Value>,
}
#[serde_with::serde_as]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Wallpaper {
/// Path to the wallpaper image file
#[serde_as(as = "ResolvedPathBuf")]
pub path: PathBuf,
/// Generate and apply Base16 theme for this wallpaper (default: true)
#[serde(skip_serializing_if = "Option::is_none")]
pub generate_theme: Option<bool>,
/// Specify Light or Dark variant for theme generation (default: Dark)
#[serde(skip_serializing_if = "Option::is_none")]
pub theme_options: Option<ThemeOptions>,
}
// serde_as must be before derive
#[serde_with::serde_as]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct WorkspaceConfig {
/// Name
pub name: String,
@@ -130,12 +190,14 @@ pub struct WorkspaceConfig {
pub layout: Option<DefaultLayout>,
/// END OF LIFE FEATURE: Custom Layout (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde_as(as = "Option<ResolvedPathBuf>")]
pub custom_layout: Option<PathBuf>,
/// Layout rules in the format of threshold => layout (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub layout_rules: Option<HashMap<usize, DefaultLayout>>,
/// END OF LIFE FEATURE: Custom layout rules (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(deserialize_with = "resolve_option_hashmap_usize_path", default)]
pub custom_layout_rules: Option<HashMap<usize, PathBuf>>,
/// Container padding (default: global)
#[serde(skip_serializing_if = "Option::is_none")]
@@ -164,6 +226,12 @@ pub struct WorkspaceConfig {
/// Specify an axis on which to flip the selected layout (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub layout_flip: Option<Axis>,
/// Determine what happens to a new window when the Floating workspace layer is active (default: Tile)
#[serde(skip_serializing_if = "Option::is_none")]
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
/// Specify a wallpaper for this workspace
#[serde(skip_serializing_if = "Option::is_none")]
pub wallpaper: Option<Wallpaper>,
}
impl From<&Workspace> for WorkspaceConfig {
@@ -239,11 +307,14 @@ impl From<&Workspace> for WorkspaceConfig {
window_container_behaviour_rules: Option::from(window_container_behaviour_rules),
float_override: *value.float_override(),
layout_flip: value.layout_flip(),
floating_layer_behaviour: value.floating_layer_behaviour(),
wallpaper: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct MonitorConfig {
/// Workspace configurations
pub workspaces: Vec<WorkspaceConfig>,
@@ -262,6 +333,12 @@ pub struct MonitorConfig {
/// Workspace padding (default: global)
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace_padding: Option<i32>,
/// Specify a wallpaper for this monitor
#[serde(skip_serializing_if = "Option::is_none")]
pub wallpaper: Option<Wallpaper>,
/// Determine what happens to a new window when the Floating workspace layer is active (default: Tile)
#[serde(skip_serializing_if = "Option::is_none")]
pub floating_layer_behaviour: Option<FloatingLayerBehaviour>,
}
impl From<&Monitor> for MonitorConfig {
@@ -297,12 +374,27 @@ impl From<&Monitor> for MonitorConfig {
window_based_work_area_offset_limit: Some(value.window_based_work_area_offset_limit()),
container_padding,
workspace_padding,
wallpaper: value.wallpaper().clone(),
floating_layer_behaviour: value.floating_layer_behaviour(),
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
/// The `komorebi.json` static configuration file reference for `v0.1.35`
#[serde_with::serde_as]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum AppSpecificConfigurationPath {
/// A single applications.json file
Single(#[serde_as(as = "ResolvedPathBuf")] PathBuf),
/// Multiple applications.json files
Multiple(#[serde_as(as = "Vec<ResolvedPathBuf>")] Vec<PathBuf>),
}
#[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.36`
pub struct StaticConfig {
/// DEPRECATED from v0.1.22: no longer required
#[serde(skip_serializing_if = "Option::is_none")]
@@ -340,7 +432,7 @@ pub struct StaticConfig {
pub mouse_follows_focus: Option<bool>,
/// Path to applications.json from komorebi-application-specific-configurations (default: None)
#[serde(skip_serializing_if = "Option::is_none")]
pub app_specific_configuration_path: Option<PathBuf>,
pub app_specific_configuration_path: Option<AppSpecificConfigurationPath>,
/// Width of the window border (default: 8)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "active_window_border_width")]
@@ -349,7 +441,7 @@ pub struct StaticConfig {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "active_window_border_offset")]
pub border_offset: Option<i32>,
/// Display an active window border (default: false)
/// Display an active window border (default: true)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "active_window_border")]
pub border: Option<bool>,
@@ -440,6 +532,7 @@ pub struct StaticConfig {
/// Komorebi status bar configuration files for multiple instances on different monitors
// this option is a little special because it is only consumed by komorebic
#[serde(skip_serializing_if = "Option::is_none")]
#[serde_as(as = "Option<Vec<ResolvedPathBuf>>")]
pub bar_configurations: Option<Vec<PathBuf>>,
/// HEAVILY DISCOURAGED: Identify applications for which komorebi should forcibly remove title bars
#[serde(skip_serializing_if = "Option::is_none")]
@@ -449,7 +542,8 @@ pub struct StaticConfig {
pub floating_window_aspect_ratio: Option<AspectRatio>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct AnimationsConfig {
/// Enable or disable animations (default: false)
pub enabled: PerAnimationPrefixConfig<bool>,
@@ -464,7 +558,8 @@ pub struct AnimationsConfig {
pub fps: Option<u64>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "palette")]
pub enum KomorebiTheme {
/// A theme from catppuccin-egui
@@ -486,6 +581,9 @@ pub enum KomorebiTheme {
/// Border colour when the container is unfocused (default: Base)
#[serde(skip_serializing_if = "Option::is_none")]
unfocused_border: Option<komorebi_themes::CatppuccinValue>,
/// Border colour when the container is unfocused and locked (default: Red)
#[serde(skip_serializing_if = "Option::is_none")]
unfocused_locked_border: Option<komorebi_themes::CatppuccinValue>,
/// Stackbar focused tab text colour (default: Green)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_focused_text: Option<komorebi_themes::CatppuccinValue>,
@@ -518,6 +616,44 @@ pub enum KomorebiTheme {
/// Border colour when the container is unfocused (default: Base01)
#[serde(skip_serializing_if = "Option::is_none")]
unfocused_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is unfocused and locked (default: Base08)
#[serde(skip_serializing_if = "Option::is_none")]
unfocused_locked_border: Option<komorebi_themes::Base16Value>,
/// Stackbar focused tab text colour (default: Base0B)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_focused_text: Option<komorebi_themes::Base16Value>,
/// Stackbar unfocused tab text colour (default: Base05)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_unfocused_text: Option<komorebi_themes::Base16Value>,
/// Stackbar tab background colour (default: Base01)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_background: Option<komorebi_themes::Base16Value>,
/// Komorebi status bar accent (default: Base0D)
#[serde(skip_serializing_if = "Option::is_none")]
bar_accent: Option<komorebi_themes::Base16Value>,
},
/// A custom Base16 theme
Custom {
/// Colours of the custom Base16 theme palette
colours: Box<komorebi_themes::Base16ColourPalette>,
/// Border colour when the container contains a single window (default: Base0D)
#[serde(skip_serializing_if = "Option::is_none")]
single_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container contains multiple windows (default: Base0B)
#[serde(skip_serializing_if = "Option::is_none")]
stack_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is in monocle mode (default: Base0F)
#[serde(skip_serializing_if = "Option::is_none")]
monocle_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the window is floating (default: Base09)
#[serde(skip_serializing_if = "Option::is_none")]
floating_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is unfocused (default: Base01)
#[serde(skip_serializing_if = "Option::is_none")]
unfocused_border: Option<komorebi_themes::Base16Value>,
/// Border colour when the container is unfocused and locked (default: Base08)
#[serde(skip_serializing_if = "Option::is_none")]
unfocused_locked_border: Option<komorebi_themes::Base16Value>,
/// Stackbar focused tab text colour (default: Base0B)
#[serde(skip_serializing_if = "Option::is_none")]
stackbar_focused_text: Option<komorebi_themes::Base16Value>,
@@ -614,7 +750,8 @@ impl StaticConfig {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct TabsConfig {
/// Width of a stackbar tab
#[serde(skip_serializing_if = "Option::is_none")]
@@ -636,7 +773,8 @@ pub struct TabsConfig {
pub font_size: Option<i32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct StackbarConfig {
/// Stackbar height
#[serde(skip_serializing_if = "Option::is_none")]
@@ -673,6 +811,9 @@ impl From<&WindowManager> for StaticConfig {
unfocused: Option::from(Colour::from(
border_manager::UNFOCUSED.load(Ordering::SeqCst),
)),
unfocused_locked: Option::from(Colour::from(
border_manager::UNFOCUSED_LOCKED.load(Ordering::SeqCst),
)),
})
};
@@ -734,7 +875,7 @@ impl From<&WindowManager> for StaticConfig {
.collect::<Vec<_>>(),
),
monitor_index_preferences: Option::from(MONITOR_INDEX_PREFERENCES.lock().clone()),
display_index_preferences: Option::from(DISPLAY_INDEX_PREFERENCES.lock().clone()),
display_index_preferences: Option::from(DISPLAY_INDEX_PREFERENCES.read().clone()),
stackbar: None,
animation: None,
theme: None,
@@ -762,7 +903,7 @@ impl StaticConfig {
}
if let Some(display_index_preferences) = &self.display_index_preferences {
let mut preferences = DISPLAY_INDEX_PREFERENCES.lock();
let mut preferences = DISPLAY_INDEX_PREFERENCES.write();
preferences.clone_from(display_index_preferences);
}
@@ -829,10 +970,7 @@ impl StaticConfig {
border_manager::BORDER_WIDTH.store(self.border_width.unwrap_or(8), Ordering::SeqCst);
border_manager::BORDER_OFFSET.store(self.border_offset.unwrap_or(-1), Ordering::SeqCst);
if let Some(enabled) = &self.border {
border_manager::BORDER_ENABLED.store(*enabled, Ordering::SeqCst);
}
border_manager::BORDER_ENABLED.store(self.border.unwrap_or(true), Ordering::SeqCst);
if let Some(colours) = &self.border_colours {
if let Some(single) = colours.single {
@@ -854,6 +992,11 @@ impl StaticConfig {
if let Some(unfocused) = colours.unfocused {
border_manager::UNFOCUSED.store(u32::from(unfocused), Ordering::SeqCst);
}
if let Some(unfocused_locked) = colours.unfocused_locked {
border_manager::UNFOCUSED_LOCKED
.store(u32::from(unfocused_locked), Ordering::SeqCst);
}
}
STYLE.store(self.border_style.unwrap_or_default());
@@ -994,144 +1137,39 @@ impl StaticConfig {
}
if let Some(theme) = &self.theme {
theme_manager::send_notification(*theme);
theme_manager::send_notification(theme.clone());
}
if let Some(path) = &self.app_specific_configuration_path {
match path.extension() {
None => {}
Some(ext) => match ext.to_string_lossy().to_string().as_str() {
"yaml" => {
tracing::info!("loading applications.yaml from: {}", path.display());
let path = resolve_home_path(path)?;
let content = std::fs::read_to_string(path)?;
let asc = ApplicationConfigurationGenerator::load(&content)?;
for mut entry in asc {
if let Some(rules) = &mut entry.ignore_identifiers {
populate_rules(
rules,
&mut ignore_identifiers,
&mut regex_identifiers,
)?;
}
if let Some(ref options) = entry.options {
let options = options.clone();
for o in options {
match o {
ApplicationOptions::ObjectNameChange => {
populate_option(
&mut entry,
&mut object_name_change_identifiers,
&mut regex_identifiers,
)?;
}
ApplicationOptions::Layered => {
populate_option(
&mut entry,
&mut layered_identifiers,
&mut regex_identifiers,
)?;
}
ApplicationOptions::TrayAndMultiWindow => {
populate_option(
&mut entry,
&mut tray_and_multi_window_identifiers,
&mut regex_identifiers,
)?;
}
ApplicationOptions::Force => {
populate_option(
&mut entry,
&mut manage_identifiers,
&mut regex_identifiers,
)?;
}
ApplicationOptions::BorderOverflow => {} // deprecated
}
}
}
}
match path {
AppSpecificConfigurationPath::Single(path) => handle_asc_file(
path,
&mut ignore_identifiers,
&mut object_name_change_identifiers,
&mut layered_identifiers,
&mut tray_and_multi_window_identifiers,
&mut manage_identifiers,
&mut floating_applications,
&mut transparency_blacklist,
&mut slow_application_identifiers,
&mut regex_identifiers,
)?,
AppSpecificConfigurationPath::Multiple(paths) => {
for path in paths {
handle_asc_file(
path,
&mut ignore_identifiers,
&mut object_name_change_identifiers,
&mut layered_identifiers,
&mut tray_and_multi_window_identifiers,
&mut manage_identifiers,
&mut floating_applications,
&mut transparency_blacklist,
&mut slow_application_identifiers,
&mut regex_identifiers,
)?
}
"json" => {
tracing::info!("loading applications.json from: {}", path.display());
let path = resolve_home_path(path)?;
let mut asc = ApplicationSpecificConfiguration::load(&path)?;
for entry in asc.values_mut() {
match entry {
AscApplicationRulesOrSchema::Schema(_) => {}
AscApplicationRulesOrSchema::AscApplicationRules(entry) => {
if let Some(rules) = &mut entry.ignore {
populate_rules(
rules,
&mut ignore_identifiers,
&mut regex_identifiers,
)?;
}
if let Some(rules) = &mut entry.manage {
populate_rules(
rules,
&mut manage_identifiers,
&mut regex_identifiers,
)?;
}
if let Some(rules) = &mut entry.floating {
populate_rules(
rules,
&mut floating_applications,
&mut regex_identifiers,
)?;
}
if let Some(rules) = &mut entry.transparency_ignore {
populate_rules(
rules,
&mut transparency_blacklist,
&mut regex_identifiers,
)?;
}
if let Some(rules) = &mut entry.tray_and_multi_window {
populate_rules(
rules,
&mut tray_and_multi_window_identifiers,
&mut regex_identifiers,
)?;
}
if let Some(rules) = &mut entry.layered {
populate_rules(
rules,
&mut layered_identifiers,
&mut regex_identifiers,
)?;
}
if let Some(rules) = &mut entry.object_name_change {
populate_rules(
rules,
&mut object_name_change_identifiers,
&mut regex_identifiers,
)?;
}
if let Some(rules) = &mut entry.slow_application {
populate_rules(
rules,
&mut slow_application_identifiers,
&mut regex_identifiers,
)?;
}
}
}
}
}
_ => {}
},
}
}
}
@@ -1144,35 +1182,7 @@ impl StaticConfig {
pub fn read(path: &PathBuf) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
let mut value: Self = serde_json::from_str(&content)?;
if let Some(path) = &mut value.app_specific_configuration_path {
*path = resolve_home_path(&*path)?;
}
if let Some(monitors) = &mut value.monitors {
for m in monitors {
for w in &mut m.workspaces {
if let Some(path) = &mut w.custom_layout {
*path = resolve_home_path(&*path)?;
}
if let Some(map) = &mut w.custom_layout_rules {
for path in map.values_mut() {
*path = resolve_home_path(&*path)?;
}
}
}
}
}
if let Some(bar_configurations) = &mut value.bar_configurations {
for path in bar_configurations {
*path = resolve_home_path(&*path)?;
}
}
Ok(value)
serde_json::from_str(&content).map_err(Into::into)
}
#[allow(clippy::too_many_lines)]
@@ -1270,7 +1280,7 @@ impl StaticConfig {
let mut wm = wm.lock();
let configs_with_preference: Vec<_> =
DISPLAY_INDEX_PREFERENCES.lock().keys().copied().collect();
DISPLAY_INDEX_PREFERENCES.read().keys().copied().collect();
let mut configs_used = Vec::new();
let mut workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
@@ -1280,7 +1290,7 @@ impl StaticConfig {
let offset = wm.work_area_offset;
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
let preferred_config_idx = {
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.read();
let c_idx = display_index_preferences.iter().find_map(|(c_idx, id)| {
(monitor
.serial_number_id()
@@ -1325,6 +1335,8 @@ impl StaticConfig {
);
monitor.set_container_padding(monitor_config.container_padding);
monitor.set_workspace_padding(monitor_config.workspace_padding);
monitor.set_wallpaper(monitor_config.wallpaper.clone());
monitor.set_floating_layer_behaviour(monitor_config.floating_layer_behaviour);
monitor.update_workspaces_globals(offset);
for (j, ws) in monitor.workspaces_mut().iter_mut().enumerate() {
@@ -1379,7 +1391,7 @@ impl StaticConfig {
.filter(|i| !configs_used.contains(i))
{
let id = {
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.read();
display_index_preferences.get(i).cloned()
};
if let (Some(id), Some(monitor_config)) =
@@ -1410,6 +1422,7 @@ impl StaticConfig {
);
m.set_container_padding(monitor_config.container_padding);
m.set_workspace_padding(monitor_config.workspace_padding);
m.set_floating_layer_behaviour(monitor_config.floating_layer_behaviour);
m.update_workspaces_globals(offset);
@@ -1439,7 +1452,7 @@ impl StaticConfig {
value.apply_globals()?;
let configs_with_preference: Vec<_> =
DISPLAY_INDEX_PREFERENCES.lock().keys().copied().collect();
DISPLAY_INDEX_PREFERENCES.read().keys().copied().collect();
let mut configs_used = Vec::new();
let mut workspace_matching_rules = WORKSPACE_MATCHING_RULES.lock();
@@ -1449,7 +1462,7 @@ impl StaticConfig {
let offset = wm.work_area_offset;
for (i, monitor) in wm.monitors_mut().iter_mut().enumerate() {
let preferred_config_idx = {
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.read();
let c_idx = display_index_preferences.iter().find_map(|(c_idx, id)| {
(monitor
.serial_number_id()
@@ -1496,6 +1509,8 @@ impl StaticConfig {
);
monitor.set_container_padding(monitor_config.container_padding);
monitor.set_workspace_padding(monitor_config.workspace_padding);
monitor.set_wallpaper(monitor_config.wallpaper.clone());
monitor.set_floating_layer_behaviour(monitor_config.floating_layer_behaviour);
monitor.update_workspaces_globals(offset);
@@ -1551,7 +1566,7 @@ impl StaticConfig {
.filter(|i| !configs_used.contains(i))
{
let id = {
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.read();
display_index_preferences.get(i).cloned()
};
if let (Some(id), Some(monitor_config)) =
@@ -1582,6 +1597,7 @@ impl StaticConfig {
);
m.set_container_padding(monitor_config.container_padding);
m.set_workspace_padding(monitor_config.workspace_padding);
m.set_floating_layer_behaviour(monitor_config.floating_layer_behaviour);
m.update_workspaces_globals(offset);
@@ -1646,6 +1662,8 @@ impl StaticConfig {
for i in 0..monitor_count {
wm.update_focused_workspace_by_monitor_idx(i)?;
let ws_idx = wm.focused_workspace_idx_for_monitor_idx(i)?;
wm.apply_wallpaper_for_monitor_workspace(i, ws_idx)?;
}
Ok(())
@@ -1715,3 +1733,201 @@ fn populate_rules(
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn handle_asc_file(
path: &PathBuf,
ignore_identifiers: &mut Vec<MatchingRule>,
object_name_change_identifiers: &mut Vec<MatchingRule>,
layered_identifiers: &mut Vec<MatchingRule>,
tray_and_multi_window_identifiers: &mut Vec<MatchingRule>,
manage_identifiers: &mut Vec<MatchingRule>,
floating_applications: &mut Vec<MatchingRule>,
transparency_blacklist: &mut Vec<MatchingRule>,
slow_application_identifiers: &mut Vec<MatchingRule>,
regex_identifiers: &mut HashMap<String, Regex>,
) -> Result<()> {
match path.extension() {
None => {}
Some(ext) => match ext.to_string_lossy().to_string().as_str() {
"yaml" => {
tracing::info!("loading applications.yaml from: {}", path.display());
let content = std::fs::read_to_string(path)?;
let asc = ApplicationConfigurationGenerator::load(&content)?;
for mut entry in asc {
if let Some(rules) = &mut entry.ignore_identifiers {
populate_rules(rules, ignore_identifiers, regex_identifiers)?;
}
if let Some(ref options) = entry.options {
let options = options.clone();
for o in options {
match o {
ApplicationOptions::ObjectNameChange => {
populate_option(
&mut entry,
object_name_change_identifiers,
regex_identifiers,
)?;
}
ApplicationOptions::Layered => {
populate_option(
&mut entry,
layered_identifiers,
regex_identifiers,
)?;
}
ApplicationOptions::TrayAndMultiWindow => {
populate_option(
&mut entry,
tray_and_multi_window_identifiers,
regex_identifiers,
)?;
}
ApplicationOptions::Force => {
populate_option(
&mut entry,
manage_identifiers,
regex_identifiers,
)?;
}
ApplicationOptions::BorderOverflow => {} // deprecated
}
}
}
}
}
"json" => {
tracing::info!("loading applications.json from: {}", path.display());
let mut asc = ApplicationSpecificConfiguration::load(path)?;
for entry in asc.values_mut() {
match entry {
AscApplicationRulesOrSchema::Schema(_) => {}
AscApplicationRulesOrSchema::AscApplicationRules(entry) => {
if let Some(rules) = &mut entry.ignore {
populate_rules(rules, ignore_identifiers, regex_identifiers)?;
}
if let Some(rules) = &mut entry.manage {
populate_rules(rules, manage_identifiers, regex_identifiers)?;
}
if let Some(rules) = &mut entry.floating {
populate_rules(rules, floating_applications, regex_identifiers)?;
}
if let Some(rules) = &mut entry.transparency_ignore {
populate_rules(rules, transparency_blacklist, regex_identifiers)?;
}
if let Some(rules) = &mut entry.tray_and_multi_window {
populate_rules(
rules,
tray_and_multi_window_identifiers,
regex_identifiers,
)?;
}
if let Some(rules) = &mut entry.layered {
populate_rules(rules, layered_identifiers, regex_identifiers)?;
}
if let Some(rules) = &mut entry.object_name_change {
populate_rules(
rules,
object_name_change_identifiers,
regex_identifiers,
)?;
}
if let Some(rules) = &mut entry.slow_application {
populate_rules(
rules,
slow_application_identifiers,
regex_identifiers,
)?;
}
}
}
}
}
_ => {}
},
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use crate::StaticConfig;
use crate::WorkspaceConfig;
#[test]
fn backwards_compat() {
let root = vec!["0.1.17", "0.1.18", "0.1.19"];
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",
];
let mut versions = vec![];
let client = reqwest::blocking::Client::new();
for version in root {
let request = client.get(format!("https://raw.githubusercontent.com/LGUG2Z/komorebi/refs/tags/v{version}/komorebi.example.json")).header("User-Agent", "komorebi-backwards-compat-test").build().unwrap();
versions.push((version, client.execute(request).unwrap().text().unwrap()));
}
for version in docs {
let request = client.get(format!("https://raw.githubusercontent.com/LGUG2Z/komorebi/refs/tags/v{version}/docs/komorebi.example.json")).header("User-Agent", "komorebi-backwards-compat-test").build().unwrap();
versions.push((version, client.execute(request).unwrap().text().unwrap()));
}
for (version, config) in versions {
println!("{version}");
StaticConfig::read_raw(&config).unwrap();
}
}
#[test]
fn deserialize_custom_layout_rules() {
// set an environment variable for testing
std::env::set_var("VAR", "VALUE");
let config = r#"
{
"name": "Test",
"custom_layout_rules": {
"1": "path/to/dir",
"2": "path/to/%VAR%"
}
}
"#;
let config = serde_json::from_str::<WorkspaceConfig>(config).unwrap();
let custom_layout_rules = config.custom_layout_rules.unwrap();
assert_eq!(
custom_layout_rules.get(&1).unwrap(),
&PathBuf::from("path/to/dir")
);
assert_eq!(
custom_layout_rules.get(&2).unwrap(),
&PathBuf::from("path/to/VALUE")
);
let config = r#"
{
"name": "Test",
}
"#;
let config = serde_json::from_str::<WorkspaceConfig>(config).unwrap();
assert_eq!(config.custom_layout_rules, None);
}
}

View File

@@ -5,11 +5,12 @@ use crate::stackbar_manager;
use crate::stackbar_manager::STACKBAR_FOCUSED_TEXT_COLOUR;
use crate::stackbar_manager::STACKBAR_TAB_BACKGROUND_COLOUR;
use crate::stackbar_manager::STACKBAR_UNFOCUSED_TEXT_COLOUR;
use crate::Colour;
use crate::KomorebiTheme;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicCell;
use komorebi_themes::colour::Colour;
use komorebi_themes::Base16Wrapper;
use std::ops::Deref;
use std::sync::atomic::Ordering;
use std::sync::OnceLock;
@@ -76,6 +77,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
monocle_border,
floating_border,
unfocused_border,
unfocused_locked_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
@@ -87,6 +89,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
monocle_border,
floating_border,
unfocused_border,
unfocused_locked_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
@@ -112,6 +115,10 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
.unwrap_or(komorebi_themes::CatppuccinValue::Base)
.color32(name.as_theme());
let unfocused_locked_border = unfocused_locked_border
.unwrap_or(komorebi_themes::CatppuccinValue::Red)
.color32(name.as_theme());
let stackbar_focused_text = stackbar_focused_text
.unwrap_or(komorebi_themes::CatppuccinValue::Green)
.color32(name.as_theme());
@@ -130,6 +137,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
monocle_border,
floating_border,
unfocused_border,
unfocused_locked_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
@@ -142,6 +150,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
monocle_border,
floating_border,
unfocused_border,
unfocused_locked_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
@@ -149,35 +158,39 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
} => {
let single_border = single_border
.unwrap_or(komorebi_themes::Base16Value::Base0D)
.color32(*name);
.color32(Base16Wrapper::Base16(*name));
let stack_border = stack_border
.unwrap_or(komorebi_themes::Base16Value::Base0B)
.color32(*name);
.color32(Base16Wrapper::Base16(*name));
let monocle_border = monocle_border
.unwrap_or(komorebi_themes::Base16Value::Base0F)
.color32(*name);
.color32(Base16Wrapper::Base16(*name));
let unfocused_border = unfocused_border
.unwrap_or(komorebi_themes::Base16Value::Base01)
.color32(*name);
.color32(Base16Wrapper::Base16(*name));
let unfocused_locked_border = unfocused_locked_border
.unwrap_or(komorebi_themes::Base16Value::Base08)
.color32(Base16Wrapper::Base16(*name));
let floating_border = floating_border
.unwrap_or(komorebi_themes::Base16Value::Base09)
.color32(*name);
.color32(Base16Wrapper::Base16(*name));
let stackbar_focused_text = stackbar_focused_text
.unwrap_or(komorebi_themes::Base16Value::Base0B)
.color32(*name);
.color32(Base16Wrapper::Base16(*name));
let stackbar_unfocused_text = stackbar_unfocused_text
.unwrap_or(komorebi_themes::Base16Value::Base05)
.color32(*name);
.color32(Base16Wrapper::Base16(*name));
let stackbar_background = stackbar_background
.unwrap_or(komorebi_themes::Base16Value::Base01)
.color32(*name);
.color32(Base16Wrapper::Base16(*name));
(
single_border,
@@ -185,6 +198,68 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
monocle_border,
floating_border,
unfocused_border,
unfocused_locked_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
)
}
KomorebiTheme::Custom {
colours,
single_border,
stack_border,
monocle_border,
floating_border,
unfocused_border,
unfocused_locked_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
..
} => {
let single_border = single_border
.unwrap_or(komorebi_themes::Base16Value::Base0D)
.color32(Base16Wrapper::Custom(colours.clone()));
let stack_border = stack_border
.unwrap_or(komorebi_themes::Base16Value::Base0B)
.color32(Base16Wrapper::Custom(colours.clone()));
let monocle_border = monocle_border
.unwrap_or(komorebi_themes::Base16Value::Base0F)
.color32(Base16Wrapper::Custom(colours.clone()));
let unfocused_border = unfocused_border
.unwrap_or(komorebi_themes::Base16Value::Base01)
.color32(Base16Wrapper::Custom(colours.clone()));
let unfocused_locked_border = unfocused_locked_border
.unwrap_or(komorebi_themes::Base16Value::Base08)
.color32(Base16Wrapper::Custom(colours.clone()));
let floating_border = floating_border
.unwrap_or(komorebi_themes::Base16Value::Base09)
.color32(Base16Wrapper::Custom(colours.clone()));
let stackbar_focused_text = stackbar_focused_text
.unwrap_or(komorebi_themes::Base16Value::Base0B)
.color32(Base16Wrapper::Custom(colours.clone()));
let stackbar_unfocused_text = stackbar_unfocused_text
.unwrap_or(komorebi_themes::Base16Value::Base05)
.color32(Base16Wrapper::Custom(colours.clone()));
let stackbar_background = stackbar_background
.unwrap_or(komorebi_themes::Base16Value::Base01)
.color32(Base16Wrapper::Custom(colours.clone()));
(
single_border,
stack_border,
monocle_border,
floating_border,
unfocused_border,
unfocused_locked_border,
stackbar_focused_text,
stackbar_unfocused_text,
stackbar_background,
@@ -198,6 +273,10 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
border_manager::FLOATING.store(u32::from(Colour::from(floating_border)), Ordering::SeqCst);
border_manager::UNFOCUSED
.store(u32::from(Colour::from(unfocused_border)), Ordering::SeqCst);
border_manager::UNFOCUSED_LOCKED.store(
u32::from(Colour::from(unfocused_locked_border)),
Ordering::SeqCst,
);
STACKBAR_TAB_BACKGROUND_COLOUR.store(
u32::from(Colour::from(stackbar_background)),
@@ -216,7 +295,7 @@ pub fn handle_notifications() -> color_eyre::Result<()> {
CURRENT_THEME.store(Some(notification.0));
border_manager::send_notification(None);
border_manager::send_force_update();
stackbar_manager::send_notification();
}

View File

@@ -10,14 +10,44 @@ use crate::animation::ANIMATION_ENABLED_PER_ANIMATION;
use crate::animation::ANIMATION_MANAGER;
use crate::animation::ANIMATION_STYLE_GLOBAL;
use crate::animation::ANIMATION_STYLE_PER_ANIMATION;
use crate::border_manager;
use crate::com::SetCloak;
use crate::core::config_generation::IdWithIdentifier;
use crate::core::config_generation::MatchingRule;
use crate::core::config_generation::MatchingStrategy;
use crate::core::ApplicationIdentifier;
use crate::core::HidingBehaviour;
use crate::core::Rect;
use crate::focus_manager;
use crate::stackbar_manager;
use crate::styles::ExtendedWindowStyle;
use crate::styles::WindowStyle;
use crate::transparency_manager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api;
use crate::windows_api::WindowsApi;
use crate::AnimationStyle;
use crate::FLOATING_APPLICATIONS;
use crate::FLOATING_WINDOW_TOGGLE_ASPECT_RATIO;
use crate::HIDDEN_HWNDS;
use crate::HIDING_BEHAVIOUR;
use crate::IGNORE_IDENTIFIERS;
use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
use crate::NO_TITLEBAR;
use crate::PERMAIGNORE_CLASSES;
use crate::REGEX_IDENTIFIERS;
use crate::SLOW_APPLICATION_COMPENSATION_TIME;
use crate::SLOW_APPLICATION_IDENTIFIERS;
use crate::WSL2_UI_PROCESSES;
use color_eyre::eyre;
use color_eyre::Result;
use crossbeam_utils::atomic::AtomicConsume;
use regex::Regex;
use serde::ser::SerializeStruct;
use serde::Deserialize;
use serde::Serialize;
use serde::Serializer;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::fmt::Display;
@@ -27,47 +57,15 @@ use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
use crate::core::config_generation::IdWithIdentifier;
use crate::core::config_generation::MatchingRule;
use crate::core::config_generation::MatchingStrategy;
use color_eyre::eyre;
use color_eyre::Result;
use crossbeam_utils::atomic::AtomicConsume;
use regex::Regex;
use schemars::JsonSchema;
use serde::ser::SerializeStruct;
use serde::Deserialize;
use serde::Serialize;
use serde::Serializer;
use strum::Display;
use strum::EnumString;
use windows::Win32::Foundation::HWND;
use crate::core::ApplicationIdentifier;
use crate::core::HidingBehaviour;
use crate::core::Rect;
use crate::styles::ExtendedWindowStyle;
use crate::styles::WindowStyle;
use crate::transparency_manager;
use crate::window_manager_event::WindowManagerEvent;
use crate::windows_api::WindowsApi;
use crate::FLOATING_APPLICATIONS;
use crate::HIDDEN_HWNDS;
use crate::HIDING_BEHAVIOUR;
use crate::IGNORE_IDENTIFIERS;
use crate::LAYERED_WHITELIST;
use crate::MANAGE_IDENTIFIERS;
use crate::NO_TITLEBAR;
use crate::PERMAIGNORE_CLASSES;
use crate::REGEX_IDENTIFIERS;
use crate::WSL2_UI_PROCESSES;
pub static MINIMUM_WIDTH: AtomicI32 = AtomicI32::new(0);
pub static MINIMUM_HEIGHT: AtomicI32 = AtomicI32::new(0);
#[derive(Debug, Default, Clone, Copy, Deserialize, JsonSchema, PartialEq)]
#[derive(Debug, Default, Clone, Copy, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Window {
pub hwnd: isize,
}
@@ -87,7 +85,8 @@ impl From<HWND> for Window {
}
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Clone, Serialize, JsonSchema)]
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct WindowDetails {
pub title: String,
pub exe: String,
@@ -299,9 +298,8 @@ impl RenderDispatcher for TransparencyRenderDispatcher {
}
}
#[derive(
Copy, Clone, Debug, Display, EnumString, Serialize, Deserialize, JsonSchema, PartialEq,
)]
#[derive(Copy, Clone, Debug, Display, EnumString, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum AspectRatio {
/// A predefined aspect ratio
@@ -316,9 +314,8 @@ impl Default for AspectRatio {
}
}
#[derive(
Copy, Clone, Debug, Default, Display, EnumString, Serialize, Deserialize, JsonSchema, PartialEq,
)]
#[derive(Copy, Clone, Debug, Default, Display, EnumString, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum PredefinedAspectRatio {
/// 21:9
Ultrawide,
@@ -480,7 +477,7 @@ impl Window {
WindowsApi::is_window_visible(self.hwnd)
}
pub fn hide(self) {
pub fn hide_with_border(self, hide_border: bool) {
let mut programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
if !programmatically_hidden_hwnds.contains(&self.hwnd) {
programmatically_hidden_hwnds.push(self.hwnd);
@@ -492,9 +489,16 @@ impl Window {
HidingBehaviour::Minimize => WindowsApi::minimize_window(self.hwnd),
HidingBehaviour::Cloak => SetCloak(self.hwnd(), 1, 2),
}
if hide_border {
border_manager::hide_border(self.hwnd);
}
}
pub fn restore(self) {
pub fn hide(self) {
self.hide_with_border(true);
}
pub fn restore_with_border(self, restore_border: bool) {
let mut programmatically_hidden_hwnds = HIDDEN_HWNDS.lock();
if let Some(idx) = programmatically_hidden_hwnds
.iter()
@@ -510,6 +514,13 @@ impl Window {
}
HidingBehaviour::Cloak => SetCloak(self.hwnd(), 1, 0),
}
if restore_border {
border_manager::show_border(self.hwnd);
}
}
pub fn restore(self) {
self.restore_with_border(true);
}
pub fn minimize(self) {
@@ -742,8 +753,8 @@ impl Window {
/// it raises it as well.
pub fn raise(self) -> Result<()> {
WindowsApi::raise_window(self.hwnd)?;
if let Some(border) = crate::border_manager::window_border(self.hwnd) {
WindowsApi::raise_window(border.hwnd)?;
if let Some(border_info) = crate::border_manager::window_border(self.hwnd) {
WindowsApi::raise_window(border_info.border_hwnd)?;
}
Ok(())
}
@@ -754,8 +765,8 @@ impl Window {
/// it lowers it as well.
pub fn lower(self) -> Result<()> {
WindowsApi::lower_window(self.hwnd)?;
if let Some(border) = crate::border_manager::window_border(self.hwnd) {
WindowsApi::lower_window(border.hwnd)?;
if let Some(border_info) = crate::border_manager::window_border(self.hwnd) {
WindowsApi::lower_window(border_info.border_hwnd)?;
}
Ok(())
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
use std::fmt::Display;
use std::fmt::Formatter;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
@@ -12,7 +11,8 @@ use crate::OBJECT_NAME_CHANGE_ON_LAUNCH;
use crate::OBJECT_NAME_CHANGE_TITLE_IGNORE_LIST;
use crate::REGEX_IDENTIFIERS;
#[derive(Debug, Copy, Clone, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "type", content = "content")]
pub enum WindowManagerEvent {
Destroy(WinEvent, Window),

View File

@@ -1,13 +1,13 @@
use color_eyre::eyre::anyhow;
use color_eyre::eyre::bail;
use color_eyre::eyre::Error;
use color_eyre::Result;
use core::ffi::c_void;
use std::collections::HashMap;
use std::collections::VecDeque;
use std::convert::TryFrom;
use std::mem::size_of;
use color_eyre::eyre::anyhow;
use color_eyre::eyre::bail;
use color_eyre::eyre::Error;
use color_eyre::Result;
use std::path::Path;
use windows::core::Result as WindowsCrateResult;
use windows::core::PCWSTR;
use windows::core::PWSTR;
@@ -47,6 +47,8 @@ use windows::Win32::Graphics::Gdi::HMONITOR;
use windows::Win32::Graphics::Gdi::MONITORENUMPROC;
use windows::Win32::Graphics::Gdi::MONITORINFOEXW;
use windows::Win32::Graphics::Gdi::MONITOR_DEFAULTTONEAREST;
use windows::Win32::System::Com::CoCreateInstance;
use windows::Win32::System::Com::CLSCTX_ALL;
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::System::Power::RegisterPowerSettingNotification;
use windows::Win32::System::Power::HPOWERNOTIFY;
@@ -72,6 +74,9 @@ use windows::Win32::UI::Input::KeyboardAndMouse::MOUSEEVENTF_LEFTUP;
use windows::Win32::UI::Input::KeyboardAndMouse::MOUSEINPUT;
use windows::Win32::UI::Input::KeyboardAndMouse::VK_LBUTTON;
use windows::Win32::UI::Input::KeyboardAndMouse::VK_MENU;
use windows::Win32::UI::Shell::DesktopWallpaper;
use windows::Win32::UI::Shell::IDesktopWallpaper;
use windows::Win32::UI::Shell::DWPOS_FILL;
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
use windows::Win32::UI::WindowsAndMessaging::BringWindowToTop;
use windows::Win32::UI::WindowsAndMessaging::CreateWindowExW;
@@ -141,6 +146,7 @@ use windows::Win32::UI::WindowsAndMessaging::WS_EX_TOPMOST;
use windows::Win32::UI::WindowsAndMessaging::WS_POPUP;
use windows::Win32::UI::WindowsAndMessaging::WS_SYSMENU;
use windows_core::BOOL;
use windows_core::HSTRING;
use crate::core::Rect;
@@ -153,6 +159,7 @@ use crate::windows_callbacks;
use crate::Window;
use crate::WindowManager;
use crate::DISPLAY_INDEX_PREFERENCES;
use crate::DUPLICATE_MONITOR_SERIAL_IDS;
use crate::MONITOR_INDEX_PREFERENCES;
macro_rules! as_ptr {
@@ -258,7 +265,30 @@ impl WindowsApi {
let monitors = &mut wm.monitors;
let monitor_usr_idx_map = &mut wm.monitor_usr_idx_map;
'read: for display in win32_display_data::connected_displays_all().flatten() {
let all_displays = win32_display_data::connected_displays_all()
.flatten()
.collect::<Vec<_>>();
let mut serial_id_map = HashMap::new();
for d in &all_displays {
if let Some(id) = &d.serial_number_id {
*serial_id_map.entry(id.clone()).or_insert(0) += 1;
}
}
for d in &all_displays {
if let Some(id) = &d.serial_number_id {
if serial_id_map.get(id).copied().unwrap_or_default() > 1 {
let mut dupes = DUPLICATE_MONITOR_SERIAL_IDS.write();
if !dupes.contains(id) {
(*dupes).push(id.clone());
}
}
}
}
'read: for mut display in all_displays {
let path = display.device_path.clone();
let (device, device_id) = if path.is_empty() {
@@ -281,6 +311,13 @@ impl WindowsApi {
}
}
if let Some(id) = &display.serial_number_id {
let dupes = DUPLICATE_MONITOR_SERIAL_IDS.read();
if dupes.contains(id) {
display.serial_number_id = None;
}
}
let m = monitor::new(
display.hmonitor,
display.size.into(),
@@ -299,7 +336,7 @@ impl WindowsApi {
}
}
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.lock();
let display_index_preferences = DISPLAY_INDEX_PREFERENCES.read();
for (index, id) in &*display_index_preferences {
if m.serial_number_id().as_ref().is_some_and(|sn| sn == id) || id.eq(m.device_id())
{
@@ -334,7 +371,7 @@ impl WindowsApi {
// Rebuild monitor index map
*monitor_usr_idx_map = HashMap::new();
let mut added_monitor_idxs = Vec::new();
for (index, id) in &*DISPLAY_INDEX_PREFERENCES.lock() {
for (index, id) in &*DISPLAY_INDEX_PREFERENCES.read() {
if let Some(m_idx) = monitors.elements().iter().position(|m| {
m.serial_number_id().as_ref().is_some_and(|sn| sn == id) || m.device_id() == id
}) {
@@ -902,7 +939,7 @@ impl WindowsApi {
pub fn exe(handle: HANDLE) -> Result<String> {
Ok(Self::exe_path(handle)?
.split('\\')
.last()
.next_back()
.ok_or_else(|| anyhow!("there is no last element"))?
.to_string())
}
@@ -971,8 +1008,18 @@ impl WindowsApi {
Ok(ex_info)
}
pub fn monitor(hmonitor: isize) -> Result<Monitor> {
pub fn monitor_device_path(hmonitor: isize) -> Option<String> {
for display in win32_display_data::connected_displays_all().flatten() {
if display.hmonitor == hmonitor {
return Some(display.device_path.clone());
}
}
None
}
pub fn monitor(hmonitor: isize) -> Result<Monitor> {
for mut display in win32_display_data::connected_displays_all().flatten() {
if display.hmonitor == hmonitor {
let path = display.device_path;
@@ -990,6 +1037,13 @@ impl WindowsApi {
let name = display.device_name.trim_start_matches(r"\\.\").to_string();
let name = name.split('\\').collect::<Vec<_>>()[0].to_string();
if let Some(id) = &display.serial_number_id {
let dupes = DUPLICATE_MONITOR_SERIAL_IDS.read();
if dupes.contains(id) {
display.serial_number_id = None;
}
}
let monitor = monitor::new(
hmonitor,
display.size.into(),
@@ -1160,7 +1214,7 @@ impl WindowsApi {
pub fn create_border_window(
name: PCWSTR,
instance: isize,
border: *const Border,
border: *mut Border,
) -> Result<isize> {
unsafe {
CreateWindowExW(
@@ -1307,4 +1361,47 @@ impl WindowsApi {
pub fn wts_register_session_notification(hwnd: isize) -> Result<()> {
unsafe { WTSRegisterSessionNotification(HWND(as_ptr!(hwnd)), 1) }.process()
}
pub fn set_wallpaper(path: &Path, hmonitor: isize) -> Result<()> {
let path = path.canonicalize()?;
let wallpaper: IDesktopWallpaper =
unsafe { CoCreateInstance(&DesktopWallpaper, None, CLSCTX_ALL)? };
let wallpaper_path = HSTRING::from(path.to_str().unwrap_or_default());
unsafe {
wallpaper.SetPosition(DWPOS_FILL)?;
}
let monitor_id = if let Some(path) = Self::monitor_device_path(hmonitor) {
PCWSTR::from_raw(HSTRING::from(path).as_ptr())
} else {
PCWSTR::null()
};
// Set the wallpaper
unsafe {
wallpaper.SetWallpaper(monitor_id, PCWSTR::from_raw(wallpaper_path.as_ptr()))?;
}
Ok(())
}
pub fn get_wallpaper(hmonitor: isize) -> Result<String> {
let wallpaper: IDesktopWallpaper =
unsafe { CoCreateInstance(&DesktopWallpaper, None, CLSCTX_ALL)? };
let monitor_id = if let Some(path) = Self::monitor_device_path(hmonitor) {
PCWSTR::from_raw(HSTRING::from(path).as_ptr())
} else {
PCWSTR::null()
};
// Set the wallpaper
unsafe {
wallpaper
.GetWallpaper(monitor_id)
.and_then(|pwstr| pwstr.to_string().map_err(|e| e.into()))
}
.process()
}
}

View File

@@ -105,12 +105,16 @@ pub extern "system" fn win_event_hook(
WinEvent::ObjectLocationChange | WinEvent::ObjectDestroy
) && !has_filtered_style(hwnd)
{
let border_window = border_manager::window_border(hwnd.0 as isize);
let border_info = border_manager::window_border(hwnd.0 as isize);
if let Some(border) = border_window {
if let Some(border_info) = border_info {
unsafe {
let _ =
SendNotifyMessageW(border.hwnd(), event, WPARAM(0), LPARAM(hwnd.0 as isize));
let _ = SendNotifyMessageW(
border_info.hwnd(),
event,
WPARAM(0),
LPARAM(hwnd.0 as isize),
);
}
}
}

View File

@@ -1,6 +1,5 @@
#![allow(clippy::use_self)]
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum::Display;
@@ -89,7 +88,8 @@ use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_EVENTID_START;
use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_END;
use windows::Win32::UI::WindowsAndMessaging::EVENT_UIA_PROPID_START;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Display, JsonSchema)]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize, Display)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[repr(u32)]
#[allow(dead_code)]
pub enum WinEvent {

File diff suppressed because it is too large Load Diff

View File

@@ -1,130 +0,0 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
use crate::border_manager;
use crate::WindowManager;
use crossbeam_channel::Receiver;
use crossbeam_channel::Sender;
use crossbeam_utils::atomic::AtomicCell;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use std::sync::Arc;
use std::sync::OnceLock;
use std::time::Duration;
use std::time::Instant;
#[derive(Copy, Clone)]
pub struct Notification {
pub monitor_idx: usize,
pub workspace_idx: usize,
}
pub static ALT_TAB_HWND: AtomicCell<Option<isize>> = AtomicCell::new(None);
lazy_static! {
pub static ref ALT_TAB_HWND_INSTANT: Arc<Mutex<Instant>> = Arc::new(Mutex::new(Instant::now()));
}
static CHANNEL: OnceLock<(Sender<Notification>, Receiver<Notification>)> = OnceLock::new();
pub fn channel() -> &'static (Sender<Notification>, Receiver<Notification>) {
CHANNEL.get_or_init(|| crossbeam_channel::bounded(1))
}
fn event_tx() -> Sender<Notification> {
channel().0.clone()
}
fn event_rx() -> Receiver<Notification> {
channel().1.clone()
}
pub fn send_notification(monitor_idx: usize, workspace_idx: usize) {
if event_tx()
.try_send(Notification {
monitor_idx,
workspace_idx,
})
.is_err()
{
tracing::warn!("channel is full; dropping notification")
}
}
pub fn listen_for_notifications(wm: Arc<Mutex<WindowManager>>) {
std::thread::spawn(move || loop {
match handle_notifications(wm.clone()) {
Ok(()) => {
tracing::warn!("restarting finished thread");
}
Err(error) => {
if cfg!(debug_assertions) {
tracing::error!("restarting failed thread: {:?}", error)
} else {
tracing::error!("restarting failed thread: {}", error)
}
}
}
});
}
pub fn handle_notifications(wm: Arc<Mutex<WindowManager>>) -> color_eyre::Result<()> {
tracing::info!("listening");
let receiver = event_rx();
let arc = wm.clone();
for notification in receiver {
tracing::info!("running reconciliation");
let mut wm = wm.lock();
let focused_monitor_idx = wm.focused_monitor_idx();
let focused_workspace_idx =
wm.focused_workspace_idx_for_monitor_idx(focused_monitor_idx)?;
let focused_pair = (focused_monitor_idx, focused_workspace_idx);
let updated_pair = (notification.monitor_idx, notification.workspace_idx);
if focused_pair != updated_pair {
wm.focus_monitor(notification.monitor_idx)?;
let mouse_follows_focus = wm.mouse_follows_focus;
if let Some(monitor) = wm.focused_monitor_mut() {
let previous_idx = monitor.focused_workspace_idx();
monitor.set_last_focused_workspace(Option::from(previous_idx));
monitor.focus_workspace(notification.workspace_idx)?;
monitor.load_focused_workspace(mouse_follows_focus)?;
}
// Drop our lock on the window manager state here to not slow down updates
drop(wm);
// Check if there was an alt-tab across workspaces in the last second
if let Some(hwnd) = ALT_TAB_HWND.load() {
if ALT_TAB_HWND_INSTANT
.lock()
.elapsed()
.lt(&Duration::from_secs(1))
{
// Sleep for 100 millis to let other events pass
std::thread::sleep(Duration::from_millis(100));
tracing::info!("focusing alt-tabbed window");
// Take a new lock on the wm and try to focus the container with
// the recorded HWND from the alt-tab
let mut wm = arc.lock();
if let Ok(workspace) = wm.focused_workspace_mut() {
// Regardless of if this fails, we need to get past this part
// to unblock the border manager below
let _ = workspace.focus_container_by_window(hwnd);
}
// Unblock the border manager
ALT_TAB_HWND.store(None);
// Send a notification to the border manager to update the borders
border_manager::send_notification(None);
}
}
}
}
Ok(())
}

Some files were not shown because too many files have changed in this diff Show More