Repository: wezterm/wezterm (language: rust)

Issue:
top level (floating) window on macos
**Is your feature request related to a problem? Please describe.**
I would love to have my wezterm window on top of other windows.

**Describe the solution you'd like**
This can be done in Swift via:
```swift
window.level = .floating
```

After a small search on github and cocoa crate repo i figure out a possible solution, and tried it in the [`hello_world`](https://github.com/servo/core-foundation-rs/blob/master/cocoa/examples/hello_world.rs) example of `cocoa-foundation-rs/cocoa`

```rust
      // create Window
      let window = NSWindow::alloc(nil)
          .initWithContentRect_styleMask_backing_defer_(
              NSRect::new(NSPoint::new(0., 0.), NSSize::new(200., 200.)),
              NSWindowStyleMask::NSTitledWindowMask,
              NSBackingStoreBuffered,
              NO,
          )
          .autorelease();


      window.setLevel_(1); // this is what we need

      window.cascadeTopLeftFromPoint_(NSPoint::new(20., 20.));
      window.center();
```

It would be super nice to have this option in the `Window` menu, something such as "Keep on top" or "Floating Window". 

I wanted to open a PR for this but I'm not familiar with the codebase and I'm not sure where this should be added. If you can provide some indications I can try implementing this myself and open a PR :) 

**Describe alternatives you've considered**
not implementing

**Additional context**

- [`cocoa-rs`#`setLevel_` documentation](https://docs.rs/cocoa/latest/cocoa/appkit/trait.NSWindow.html#tymethod.setLevel_) 

- [Official Apple `CGWindowLevelKey` documentation](https://developer.apple.com/documentation/coregraphics/cgwindowlevelkey)
- [`NSWindow.Level` documentation](https://developer.apple.com/documentation/appkit/nswindow/level)


Discussion / hints:
Demo 

https://github.com/wez/wezterm/assets/16429579/cee4403c-73cb-4992-9a5e-aa99c66b2abb



Fix patch:
diff --git a/config/src/keyassignment.rs b/config/src/keyassignment.rs
index 034920e571a..3479f1c8f59 100644
--- a/config/src/keyassignment.rs
+++ b/config/src/keyassignment.rs
@@ -1,5 +1,6 @@
 use crate::default_true;
 use crate::keys::KeyNoAction;
+use crate::window::WindowLevel;
 use luahelper::impl_lua_conversion_dynamic;
 use ordered_float::NotNan;
 use portable_pty::CommandBuilder;
@@ -505,6 +506,9 @@ pub enum KeyAssignment {
     SpawnTab(SpawnTabDomain),
     SpawnWindow,
     ToggleFullScreen,
+    ToggleAlwaysOnTop,
+    ToggleAlwaysOnBottom,
+    SetWindowLevel(WindowLevel),
     CopyTo(ClipboardCopyDestination),
     CopyTextTo {
         text: String,
diff --git a/config/src/lib.rs b/config/src/lib.rs
index 9b1e5f07131..e4928d31c6b 100644
--- a/config/src/lib.rs
+++ b/config/src/lib.rs
@@ -39,6 +39,7 @@ mod tls;
 mod units;
 mod unix;
 mod version;
+pub mod window;
 mod wsl;
 
 pub use crate::config::*;
diff --git a/config/src/window.rs b/config/src/window.rs
new file mode 100644
index 00000000000..8f34e3866d2
--- /dev/null
+++ b/config/src/window.rs
@@ -0,0 +1,9 @@
+use wezterm_dynamic::{FromDynamic, ToDynamic};
+
+#[derive(Debug, Default, Clone, ToDynamic, PartialEq, Eq, FromDynamic)]
+pub enum WindowLevel {
+    AlwaysOnBottom = -1,
+    #[default]
+    Normal = 0,
+    AlwaysOnTop = 3,
+}
diff --git a/docs/config/lua/keyassignment/SetWindowLevel.md b/docs/config/lua/keyassignment/SetWindowLevel.md
new file mode 100644
index 00000000000..50179eb24b3
--- /dev/null
+++ b/docs/config/lua/keyassignment/SetWindowLevel.md
@@ -0,0 +1,35 @@
+# `SetWindowLevel`
+
+{{since('nightly')}}
+
+Set window level specified by the argument value. eg: `AlwaysOnTop` keeps the current window on top of other windows.
+
+Accepted values: 
+
+ * `"AlwaysOnBottom"`
+ * `"Normal"` (this is the default)
+ * `"AlwaysOnTop"`
+
+```lua
+config.keys = {
+    {
+        key = '[',
+        mods = 'CMD',
+        action = wezterm.action.SetWindowLevel("AlwaysOnBottom")
+    }, 
+    {
+        key = '0',
+        mods = 'CMD|SHIFT',
+        action = wezterm.action.SetWindowLevel("Normal")
+    }, 
+    {
+        key = ']',
+        mods = 'CMD',
+        action = wezterm.action.SetWindowLevel("AlwaysOnTop")
+    }, 
+}
+```
+
+!!! note 
+    This functionality is currently only implemented on macOS. 
+    The assigned values for window level will have no effect on other operating systems.
diff --git a/docs/config/lua/keyassignment/ToggleAlwaysOnBottom.md b/docs/config/lua/keyassignment/ToggleAlwaysOnBottom.md
new file mode 100644
index 00000000000..0ef226e343e
--- /dev/null
+++ b/docs/config/lua/keyassignment/ToggleAlwaysOnBottom.md
@@ -0,0 +1,19 @@
+# `ToggleAlwaysOnBottom`
+
+{{since('nightly')}}
+
+Toggles the window to remain behind all other windows.
+
+```lua
+config.keys = {
+  {
+    key = ']',
+    mods = 'CMD|SHIFT',
+    action = wezterm.action.ToggleAlwaysOnBottom,
+  },
+}
+```
+
+!!! note
+    This functionality is currently only implemented on macOS. 
+    The assigned values for window level will have no effect on other operating systems.
diff --git a/docs/config/lua/keyassignment/ToggleAlwaysOnTop.md b/docs/config/lua/keyassignment/ToggleAlwaysOnTop.md
new file mode 100644
index 00000000000..c4626320355
--- /dev/null
+++ b/docs/config/lua/keyassignment/ToggleAlwaysOnTop.md
@@ -0,0 +1,19 @@
+# `ToggleAlwaysOnTop`
+
+{{since('nightly')}}
+
+Toggles the window between floating and non-floating states to stay on top of other windows.
+
+```lua
+config.keys = {
+  {
+    key = ']',
+    mods = 'CMD|SHIFT',
+    action = wezterm.action.ToggleAlwaysOnTop,
+  },
+}
+```
+
+!!! note 
+    This functionality is currently only implemented on macOS. 
+    The assigned values for window level will have no effect on other operating systems.
diff --git a/wezterm-gui/src/commands.rs b/wezterm-gui/src/commands.rs
index 5430f18087e..e060e64ba7c 100644
--- a/wezterm-gui/src/commands.rs
+++ b/wezterm-gui/src/commands.rs
@@ -1,5 +1,6 @@
 use crate::inputmap::InputMap;
 use config::keyassignment::*;
+use config::window::WindowLevel;
 use config::{ConfigHandle, DeferredKeyCode};
 use mux::domain::DomainState;
 use mux::Mux;
@@ -681,6 +682,47 @@ pub fn derive_command_from_key_assignment(action: &KeyAssignment) -> Option<Comm
             menubar: &["View"],
             icon: Some("md_fullscreen"),
         },
+        ToggleAlwaysOnTop => CommandDef {
+            brief: "Toggle always on Top".into(),
+            doc: "Toggles the window between floating and non-floating states to stay on top of other windows.".into(),
+            keys: vec![],
+            args: &[ArgType::ActiveWindow],
+            menubar: &["Window"],
+            icon: None,
+
+        },
+        ToggleAlwaysOnBottom => CommandDef {
+            brief: "Toggle always on Bottom".into(),
+            doc: "Toggles the window to remain behind all other windows.".into(),
+            keys: vec![],
+            args: &[ArgType::ActiveWindow],
+            menubar: &["Window"],
+            icon: None,
+        },
+        SetWindowLevel(WindowLevel::AlwaysOnTop) => CommandDef {
+            brief: "Always on Top".into(),
+            doc: "Set the window level to be on top of other windows.".into(),
+            keys: vec![],
+            args: &[ArgType::ActiveWindow],
+            menubar: &["Window", "Level"],
+            icon: None,
+        },
+        SetWindowLevel(WindowLevel::Normal) => CommandDef {
+            brief: "Normal".into(),
+            doc: "Set window level to normal".into(),
+            keys: vec![],
+            args: &[ArgType::ActiveWindow],
+            menubar: &["Window", "Level"],
+            icon: None,
+        },
+        SetWindowLevel(WindowLevel::AlwaysOnBottom) => CommandDef {
+            brief: "Always on Bottom".into(),
+            doc: "Set window to remain behind all other windows.".into(),
+            keys: vec![],
+            args: &[ArgType::ActiveWindow],
+            menubar: &["Window", "Level"],
+            icon: None,
+        },
         Hide => CommandDef {
             brief: "Hide/Minimize Window".into(),
             doc: "Hides/Mimimizes the current window".into(),
@@ -2016,6 +2058,11 @@ fn compute_default_actions() -> Vec<KeyAssignment> {
         ScrollToBottom,
         // ----------------- Window
         ToggleFullScreen,
+        ToggleAlwaysOnTop,
+        ToggleAlwaysOnBottom,
+        SetWindowLevel(WindowLevel::AlwaysOnBottom),
+        SetWindowLevel(WindowLevel::Normal),
+        SetWindowLevel(WindowLevel::AlwaysOnTop),
         Hide,
         Search(Pattern::CurrentSelectionOrEmptyString),
         PaneSelect(PaneSelectArguments {
diff --git a/wezterm-gui/src/termwindow/mod.rs b/wezterm-gui/src/termwindow/mod.rs
index 8106f4f5fcd..36553723b05 100644
--- a/wezterm-gui/src/termwindow/mod.rs
+++ b/wezterm-gui/src/termwindow/mod.rs
@@ -32,6 +32,7 @@ use config::keyassignment::{
     KeyAssignment, PaneDirection, Pattern, PromptInputLine, QuickSelectArguments,
     RotationDirection, SpawnCommand, SplitSize,
 };
+use config::window::WindowLevel;
 use config::{
     configuration, AudibleBell, ConfigHandle, Dimension, DimensionContext, FrontEndSelection,
     GeometryOrigin, GuiPosition, TermConfig, WindowCloseConfirmation,
@@ -2507,6 +2508,36 @@ impl TermWindow {
             ToggleFullScreen => {
                 self.window.as_ref().unwrap().toggle_fullscreen();
             }
+            ToggleAlwaysOnTop => {
+                let window = self.window.clone().unwrap();
+                let current_level = self.window_state.as_window_level();
+
+                match current_level {
+                    WindowLevel::AlwaysOnTop => {
+                        window.set_window_level(WindowLevel::Normal);
+                    }
+                    WindowLevel::AlwaysOnBottom | WindowLevel::Normal => {
+                        window.set_window_level(WindowLevel::AlwaysOnTop);
+                    }
+                }
+            }
+            ToggleAlwaysOnBottom => {
+                let window = self.window.clone().unwrap();
+                let current_level = self.window_state.as_window_level();
+
+                match current_level {
+                    WindowLevel::AlwaysOnBottom => {
+                        window.set_window_level(WindowLevel::Normal);
+                    }
+                    WindowLevel::AlwaysOnTop | WindowLevel::Normal => {
+                        window.set_window_level(WindowLevel::AlwaysOnBottom);
+                    }
+                }
+            }
+            SetWindowLevel(level) => {
+                let window = self.window.clone().unwrap();
+                window.set_window_level(level.clone());
+            }
             CopyTo(dest) => {
                 let text = self.selection_text(pane);
                 self.copy_to_clipboard(*dest, text);
diff --git a/window/src/lib.rs b/window/src/lib.rs
index a2512d024ca..e799fb98583 100644
--- a/window/src/lib.rs
+++ b/window/src/lib.rs
@@ -1,5 +1,6 @@
 use async_trait::async_trait;
 use bitflags::bitflags;
+use config::window::WindowLevel;
 use config::{ConfigHandle, Dimension, GeometryOrigin};
 use promise::Future;
 use std::any::Any;
@@ -110,6 +111,10 @@ bitflags! {
         /// Minimized or in some kind of off-screen state. Cannot be repainted
         /// while in this state.
         const HIDDEN = 1<<3;
+        /// Always on top (floating) window
+        const ALWAYS_ON_TOP = 1<<4;
+        /// Always on bottom (docked) window
+        const ALWAYS_ON_BOTTOM = 1<<5;
     }
 }
 
@@ -121,6 +126,16 @@ impl WindowState {
     pub fn can_paint(self) -> bool {
         !self.contains(Self::HIDDEN)
     }
+
+    pub fn as_window_level(self) -> WindowLevel {
+        if self.contains(Self::ALWAYS_ON_TOP) {
+            WindowLevel::AlwaysOnTop
+        } else if self.contains(Self::ALWAYS_ON_BOTTOM) {
+            WindowLevel::AlwaysOnBottom
+        } else {
+            WindowLevel::Normal
+        }
+    }
 }
 
 #[derive(Debug, Clone)]
@@ -294,6 +309,9 @@ pub trait WindowOps {
     /// Set some text in the clipboard
     fn set_clipboard(&self, clipboard: Clipboard, text: String);
 
+    /// Set window level. Depending on the environment and user preferences
+    fn set_window_level(&self, _level: WindowLevel) {}
+
     /// Set the icon for the window.
     /// Depending on the system this may be shown in its titlebar
     /// and/or in the task manager/task switcher
diff --git a/window/src/os/macos/window.rs b/window/src/os/macos/window.rs
index 32c5e6bf236..a3436603c7d 100644
--- a/window/src/os/macos/window.rs
+++ b/window/src/os/macos/window.rs
@@ -26,6 +26,7 @@ use cocoa::foundation::{
     NSArray, NSAutoreleasePool, NSFastEnumeration, NSInteger, NSNotFound, NSPoint, NSRect, NSSize,
     NSUInteger,
 };
+use config::window::WindowLevel;
 use config::ConfigHandle;
 use core_foundation::base::{CFTypeID, TCFType};
 use core_foundation::bundle::{CFBundleGetBundleWithIdentifier, CFBundleGetFunctionPointerForName};
@@ -664,6 +665,26 @@ unsafe impl HasRawWindowHandle for Window {
     }
 }
 
+/// @see https://developer.apple.com/documentation/appkit/nswindow/level
+pub type NSWindowLevel = i64;
+
+pub fn nswindow_level_to_window_level(nswindow_level: NSWindowLevel) -> WindowLevel {
+    match nswindow_level {
+        -1 => WindowLevel::AlwaysOnBottom,
+        0 => WindowLevel::Normal,
+        3 => WindowLevel::AlwaysOnTop,
+        _ => panic!("Invalid window level: {}", nswindow_level),
+    }
+}
+
+pub fn window_level_to_nswindow_level(level: WindowLevel) -> NSWindowLevel {
+    match level {
+        WindowLevel::AlwaysOnBottom => -1,
+        WindowLevel::Normal => 0,
+        WindowLevel::AlwaysOnTop => 3,
+    }
+}
+
 #[async_trait(?Send)]
 impl WindowOps for Window {
     async fn enable_opengl(&self) -> anyhow::Result<Rc<glium::backend::Context>> {
@@ -745,6 +766,13 @@ impl WindowOps for Window {
         });
     }
 
+    fn set_window_level(&self, level: WindowLevel) {
+        Connection::with_window_inner(self.id, move |inner| {
+            inner.set_window_level(level);
+            Ok(())
+        });
+    }
+
     fn set_inner_size(&self, width: usize, height: usize) {
         Connection::with_window_inner(self.id, move |inner| {
             inner.set_inner_size(width, height);
@@ -1149,6 +1177,14 @@ impl WindowInner {
         }
     }
 
+    fn set_window_level(&mut self, level: WindowLevel) {
+        unsafe {
+            NSWindow::setLevel_(*self.window, window_level_to_nswindow_level(level));
+            // Dispatch a resize event with the updated window state
+            WindowView::did_resize(&mut **self.view, sel!(windowDidResize:), nil);
+        }
+    }
+
     fn set_inner_size(&mut self, width: usize, height: usize) {
         unsafe {
             let frame = NSView::frame(*self.view as *mut _);
@@ -2752,6 +2788,27 @@ impl WindowView {
                     unsafe { msg_send![*window, isZoomed] }
                 });
 
+            let window_level = inner
+                .window
+                .as_ref()
+                .map(|window| {
+                    let level = unsafe { window.load().level() };
+                    nswindow_level_to_window_level(level)
+                })
+                .unwrap_or_default();
+
+            let level_state = match window_level {
+                WindowLevel::AlwaysOnBottom => WindowState::ALWAYS_ON_BOTTOM,
+                WindowLevel::AlwaysOnTop => WindowState::ALWAYS_ON_TOP,
+                WindowLevel::Normal => WindowState::default(),
+            };
+
+            let screen_state = match (is_full_screen, is_zoomed) {
+                (true, _) => WindowState::FULL_SCREEN,
+                (_, true) => WindowState::MAXIMIZED,
+                _ => WindowState::default(),
+            };
+
             let dpi = inner
                 .window
                 .as_ref()
@@ -2768,13 +2825,7 @@ impl WindowView {
                     pixel_height: height as usize,
                     dpi,
                 },
-                window_state: if is_full_screen {
-                    WindowState::FULL_SCREEN
-                } else if is_zoomed {
-                    WindowState::MAXIMIZED
-                } else {
-                    WindowState::default()
-                },
+                window_state: screen_state | level_state,
                 live_resizing,
             });
         }


Test patch: