Skip to content

Commit b126919

Browse files
authored
feat: new config file and settings window (#27)
1 parent 2199325 commit b126919

19 files changed

Lines changed: 856 additions & 625 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@
44

55
### Added
66

7-
- Add button to show active workspace layout and click to cycle through available layouts.
7+
- Added new config at `~/.config/komorebi-switcher.toml`
8+
- Added new "Settings" menu item in Right click menu and tray icon.
9+
- Added button to show active workspace layout and click to cycle through available layouts (Enable it through Settings or config).
810

911
### Changed
1012

11-
- Config is saved to `~/.config/komorebi-switcher.toml` instead of being hidden in Windows Registry and to also support new config options on macOS.
13+
- Migrated config options stored in Windows Registry to the new TOML config.
1214
- Active workspace indicator will no longer appear on top of the switcher button and will always be at the bottom of it.
1315

16+
### Removed
17+
18+
- Removed "Move and Resize" menu items from Right click menu and tray icon.
19+
1420
## [0.9.2] - 2026-01-06
1521

1622
### Fixed

README.md

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,31 @@ Or using Homebrew (macOS):
2929
brew install amrbashir/tap/komorebi-switcher
3030
```
3131

32-
## Usage
33-
34-
- <kbd>Left Click</kbd> any workspace to switch to it.
35-
- <kbd>Right Click</kbd> to open the context menu:
36-
37-
- **Move & Resize** (Windows): Open the move and resize dialog.
38-
39-
![Move and Resize panel](assets/screenshots/move-resize-panel.png)
40-
41-
- **Refresh** (Windows): Force recreate switcher windows.
42-
- **Quit**: Quit the app
43-
44-
> [!TIP]
45-
> You can also open the context menu from the tray icon on Windows.
32+
## Config
33+
34+
The config is located at `~/.config/komorebi-switcher.toml`. You can edit this file directly
35+
or use the settings window accessible from the context menu.
36+
37+
```toml
38+
# Global settings
39+
show_layout_button = false
40+
41+
# Settings for each monitor (Windows only for now)
42+
# Syntax is [monitors.<id>] where <id> is one of:
43+
# - serial_number_id
44+
# - device_id
45+
# - name
46+
# The app will try to match in the above order, depending on what info is available,
47+
# Run `komorebic monitor-information` to get info about your monitors
48+
[monitors.0]
49+
x = 378
50+
y = 1
51+
width = 402
52+
height = 65
53+
auto_width = true
54+
auto_height = true
55+
show_layout_button = false # Can be removed to use the global setting
56+
```
4657

4758
## Development
4859

-62.4 KB
Binary file not shown.

src/config.rs

Lines changed: 22 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,17 @@ pub struct WindowConfig {
1212
pub height: i32,
1313
pub auto_width: bool,
1414
pub auto_height: bool,
15+
#[serde(default)]
16+
pub show_layout_button: Option<bool>,
1517
}
1618

1719
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1820
pub struct Config {
21+
#[serde(skip_serializing_if = "HashMap::is_empty", default)]
1922
pub monitors: HashMap<String, WindowConfig>,
23+
24+
#[serde(default)]
25+
pub show_layout_button: bool,
2026
}
2127

2228
impl Config {
@@ -46,16 +52,19 @@ impl Config {
4652
config_file.display()
4753
);
4854

55+
#[allow(unused_mut)]
4956
let mut config = Config::default();
57+
5058
#[cfg(target_os = "windows")]
5159
{
5260
tracing::info!("Migrating config from Windows registry if any");
5361

5462
let migrated = Self::migrate_from_registry()?;
5563
config.monitors = migrated;
56-
config.save()?;
5764
}
5865

66+
config.save()?;
67+
5968
Ok(config)
6069
}
6170
}
@@ -72,12 +81,19 @@ impl Config {
7281
Ok(())
7382
}
7483

75-
pub fn get(&self, monitor_id: &str) -> Option<&WindowConfig> {
76-
self.monitors.get(monitor_id)
84+
#[allow(dead_code)]
85+
pub fn get_monitor(&self, monitor_id: &str) -> WindowConfig {
86+
self.monitors.get(monitor_id).copied().unwrap_or_default()
7787
}
7888

79-
pub fn set(&mut self, monitor_id: String, config: WindowConfig) {
80-
self.monitors.insert(monitor_id, config);
89+
#[allow(dead_code)]
90+
pub fn get_monitor_or_default(&mut self, monitor_id: &str) -> &mut WindowConfig {
91+
self.monitors.entry(monitor_id.to_string()).or_default()
92+
}
93+
94+
#[allow(dead_code)]
95+
pub fn set_monitor(&mut self, monitor_id: &str, config: WindowConfig) {
96+
self.monitors.insert(monitor_id.to_string(), config);
8197
}
8298
}
8399

@@ -122,52 +138,11 @@ impl Config {
122138
height,
123139
auto_width,
124140
auto_height,
141+
show_layout_button: None,
125142
},
126143
);
127144
}
128145

129146
Ok(monitors)
130147
}
131148
}
132-
133-
#[cfg(target_os = "windows")]
134-
impl WindowConfig {
135-
pub fn apply(&mut self, hwnd: windows::Win32::Foundation::HWND) -> anyhow::Result<()> {
136-
use windows::Win32::Foundation::*;
137-
use windows::Win32::UI::WindowsAndMessaging::*;
138-
139-
let height = if self.auto_height {
140-
let parent = unsafe { GetParent(hwnd) }?;
141-
let mut rect = RECT::default();
142-
unsafe { GetClientRect(parent, &mut rect) }?;
143-
rect.bottom - rect.top
144-
} else {
145-
self.height
146-
};
147-
148-
let width = if self.auto_width {
149-
let child = unsafe { GetWindow(hwnd, GW_CHILD) }?;
150-
let mut rect = RECT::default();
151-
unsafe { GetClientRect(child, &mut rect) }?;
152-
rect.right - rect.left
153-
} else {
154-
self.width
155-
};
156-
157-
self.width = width;
158-
self.height = height;
159-
160-
unsafe {
161-
SetWindowPos(
162-
hwnd,
163-
None,
164-
self.x,
165-
self.y,
166-
width,
167-
height,
168-
SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED,
169-
)
170-
.map_err(Into::into)
171-
}
172-
}
173-
}

src/macos/mod.rs

Lines changed: 35 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -16,44 +16,33 @@ use objc2_foundation::{
1616

1717
use self::workspace_button::WorkspaceButton;
1818
use self::workspaces_stack_view::WorkspacesStackView;
19+
use crate::config::Config;
1920
use crate::macos::layout_button::LayoutButton;
21+
use crate::macos::windows::settings::SettingsWindowController;
2022

2123
mod layout_button;
24+
mod windows;
2225
mod workspace_button;
2326
mod workspaces_stack_view;
2427

25-
#[derive(Debug)]
28+
#[derive(Default)]
2629
pub struct AppDelegateIvars {
2730
ns_status_item: OnceCell<Retained<NSStatusItem>>,
2831
ns_stack_view: OnceCell<Retained<WorkspacesStackView>>,
2932
buttons: RefCell<Vec<Retained<NSView>>>,
30-
}
31-
32-
impl Default for AppDelegateIvars {
33-
fn default() -> Self {
34-
Self {
35-
ns_status_item: OnceCell::new(),
36-
ns_stack_view: OnceCell::new(),
37-
buttons: RefCell::new(Vec::new()),
38-
}
39-
}
33+
config: OnceCell<Config>,
34+
settings_window: OnceCell<Retained<windows::settings::SettingsWindowController>>,
4035
}
4136

4237
define_class!(
43-
// SAFETY:
44-
// - The superclass NSObject does not have any subclassing requirements.
45-
// - `Delegate` does not implement `Drop`.
4638
#[unsafe(super = NSObject)]
4739
#[thread_kind = MainThreadOnly]
4840
#[ivars = AppDelegateIvars]
4941
pub struct AppDelegate;
5042

51-
// SAFETY: `NSObjectProtocol` has no safety requirements.
5243
unsafe impl NSObjectProtocol for AppDelegate {}
5344

54-
// SAFETY: `NSApplicationDelegate` has no safety requirements.
5545
unsafe impl NSApplicationDelegate for AppDelegate {
56-
// SAFETY: The signature is correct.
5746
#[unsafe(method(applicationDidFinishLaunching:))]
5847
fn did_finish_launching(&self, notification: &NSNotification) {
5948
let mtm = self.mtm();
@@ -64,41 +53,36 @@ define_class!(
6453
.downcast::<NSApplication>()
6554
.unwrap();
6655

67-
// Set the activation policy to Accessory to hide the dock icon and menu bar.
6856
app.setActivationPolicy(NSApplicationActivationPolicy::Accessory);
6957

70-
// Activate the application.
71-
// Required when launching unbundled (as is done with Cargo).
7258
#[allow(deprecated)]
7359
#[cfg(debug_assertions)]
7460
app.activateIgnoringOtherApps(true);
7561

7662
let komorebi_state = crate::komorebi::read_state().unwrap_or_default();
7763

78-
// create status bar item
7964
let ns_status_bar = NSStatusBar::systemStatusBar();
8065
let ns_status_item = ns_status_bar.statusItemWithLength(NSVariableStatusItemLength);
8166

82-
// Create stack view for horizontal button layout
8367
let stack_view = {
8468
let stack = WorkspacesStackView::new(mtm);
8569
stack.setOrientation(NSUserInterfaceLayoutOrientation::Horizontal);
8670
stack.setSpacing(2.0);
8771
stack
8872
};
8973

90-
// Add stack view to status item button
9174
if let Some(btn) = ns_status_item.button(mtm) {
9275
btn.addSubview(&stack_view);
9376
}
9477

9578
let _ = self.ivars().ns_status_item.set(ns_status_item);
9679
let _ = self.ivars().ns_stack_view.set(stack_view);
9780

98-
// Create initial workspace buttons
81+
let config = Config::load().unwrap_or_default();
82+
let _ = self.ivars().config.set(config);
83+
9984
self.update_workspace_buttons(komorebi_state);
10085

101-
// Listen for komorebi state changes on a separate thread
10286
std::thread::spawn(|| {
10387
crate::komorebi::listen_for_state(|new_state| {
10488
Queue::main().exec_async(|| AppDelegate::dispatch_new_state(new_state));
@@ -111,68 +95,69 @@ define_class!(
11195
impl AppDelegate {
11296
pub fn new(mtm: MainThreadMarker) -> Retained<Self> {
11397
let this = Self::alloc(mtm).set_ivars(AppDelegateIvars::default());
114-
// SAFETY: The signature of `NSObject`'s `init` method is correct.
11598
unsafe { msg_send![super(this), init] }
11699
}
117100

118101
fn dispatch_new_state(state: crate::komorebi::State) {
119-
// SAFETY: This is called on the main thread using `Queue::main()`.
120102
let mtm = MainThreadMarker::new().unwrap();
121103
let app = NSApp(mtm);
122-
// SAFETY: We have set a delegate for the application.
123104
let delegate = app.delegate().unwrap();
124105
if let Ok(delegate) = delegate.downcast::<Self>() {
125106
delegate.update_workspace_buttons(state);
126107
}
127108
}
128109

110+
fn show_or_create_settings_window(&self) {
111+
if let Some(existing) = self.ivars().settings_window.get() {
112+
existing.show();
113+
return;
114+
}
115+
116+
let mtm = self.mtm();
117+
let config = self.ivars().config.get().unwrap().clone();
118+
119+
let window_controller = SettingsWindowController::new(mtm, config);
120+
121+
let _ = self.ivars().settings_window.set(window_controller);
122+
self.ivars().settings_window.get().unwrap().show();
123+
}
124+
129125
fn update_workspace_buttons(&self, state: crate::komorebi::State) {
130126
let mtm = self.mtm();
131-
// SAFETY: We have initialized these ivars in `did_finish_launching`.
132127
let stack_view = self.ivars().ns_stack_view.get().unwrap();
133128
let mut views = self.ivars().buttons.borrow_mut();
129+
let config = self.ivars().config.get().unwrap();
134130

135-
// Remove all existing buttons from stack view
136131
for button in views.iter() {
137132
button.removeFromSuperview();
138133
}
139134

140135
views.clear();
141136

142-
// Get first monitor (we only support one for now)
143137
let Some(monitor) = state.monitors.first() else {
144138
return;
145139
};
146140

147-
// Create new buttons for all workspaces
148141
for workspace in &monitor.workspaces {
149142
let workspace_button = WorkspaceButton::new(mtm, workspace);
150143
stack_view.addArrangedSubview(&workspace_button);
151-
152-
// Store button
153144
views.push(workspace_button.downcast().unwrap());
154145
}
155146

156-
// show layout button for focused workspace
157-
if let Some(focused_ws) = monitor.focused_workspace() {
158-
let separator = NSTextField::labelWithString(ns_string!("|"), mtm);
159-
separator.setAlignment(NSTextAlignment::Center);
160-
stack_view.addArrangedSubview(&separator);
147+
if config.show_layout_button {
148+
if let Some(focused_ws) = monitor.focused_workspace() {
149+
let separator = NSTextField::labelWithString(ns_string!("|"), mtm);
150+
separator.setAlignment(NSTextAlignment::Center);
151+
stack_view.addArrangedSubview(&separator);
152+
views.push(separator.downcast().unwrap());
161153

162-
// Store separator
163-
views.push(separator.downcast().unwrap());
164-
165-
let layout_button = LayoutButton::new(mtm, focused_ws);
166-
stack_view.addArrangedSubview(&layout_button);
167-
168-
// Store button
169-
views.push(layout_button.downcast().unwrap());
154+
let layout_button = LayoutButton::new(mtm, focused_ws);
155+
stack_view.addArrangedSubview(&layout_button);
156+
views.push(layout_button.downcast().unwrap());
157+
}
170158
}
171159

172-
// SAFETY: We have initialized this ivar in `did_finish_launching`.
173160
let ns_status_item = self.ivars().ns_status_item.get().unwrap();
174-
175-
// Update status item button frame to match new stack view size
176161
if let Some(btn) = ns_status_item.button(mtm) {
177162
let fitting_size = stack_view.fittingSize();
178163
let size = NSSize::new(fitting_size.width, WorkspaceButton::HEIGHT);
@@ -184,7 +169,6 @@ impl AppDelegate {
184169
}
185170

186171
pub fn run() -> anyhow::Result<()> {
187-
// SAFETY: `run` is the main entry point and is called on the main thread.
188172
let mtm = MainThreadMarker::new().unwrap();
189173

190174
let app = NSApplication::sharedApplication(mtm);

src/macos/windows/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod settings;

src/macos/windows/settings/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
mod view_controller;
2+
mod window_controller;
3+
4+
pub use view_controller::SettingsViewController;
5+
pub use window_controller::SettingsWindowController;

0 commit comments

Comments
 (0)