The rectangular screen areas managed by Jwno are called frames, and they are tidily organized in a tree-like structure, named the frame tree. Despite the name, that tree also contains window objects, and some stuff representing virtual desktops and monitors etc. - I just couldn't come up with a better name, *sigh*.
You can dump the entire frame tree in the REPL:
(:dump-subtree (get-in jwno/context [:window-manager :root]))
And the output would look like this:
Root Container
Virtual Desktop (name="Desktop 1", id="{D76896BE-1324-C415-9A47-4D3F09DECEF2}")
Monitor (primary=true,dir=horizontal,work-area={l:0,t:0,r:1920,b:1080},dpi=(96 96),device="\\\\.\\DISPLAY1")
Frame (dir=none,rect={l:10,t:10,r:960,b:1070})
Window (hwnd=<pointer 0x0000000705FE>)
Name: D:\w\janet_code\jwno\build\jwno.exe
Class: ConsoleWindowClass
Exe: D:\w\janet_code\jwno\build\jwno.exe
Rect: {l:13,t:20,r:957,b:1067}
Extended Frame Bounds: {l:20,t:20,r:950,b:1060}
Virtual Desktop ID: {D76896BE-1324-C415-9A47-4D3F09DECEF2}
Frame (dir=none,rect={l:960,t:10,r:1910,b:1070})
Window (hwnd=<pointer 0x00000010084C>)
Name: frame-tree.mdz
Class: Emacs
Exe: D:\pf\emacs\bin\emacs.exe
Rect: {l:963,t:20,r:1907,b:1067}
Extended Frame Bounds: {l:970,t:20,r:1900,b:1060}
Virtual Desktop ID: {D76896BE-1324-C415-9A47-4D3F09DECEF2}
Monitor (primary=false,dir=none,work-area={l:-1440,t:0,r:0,b:1080},dpi=(144 144),device="\\\\.\\DISPLAY24")
Window (hwnd=<pointer 0x00000008028C>)
Name: pwsh.exe
Class: CASCADIA_HOSTING_WINDOW_CLASS
Exe: C:\Program Files\WindowsApps\Microsoft.WindowsTerminal_1.21.2911.0_x64__8wekyb3d8bbwe\WindowsTerminal.exe
Rect: {l:-1420,t:30,r:-20,b:1060}
Extended Frame Bounds: {l:-1410,t:30,r:-30,b:1050}
Virtual Desktop ID: {D76896BE-1324-C415-9A47-4D3F09DECEF2}
Virtual Desktop (name="Desktop 3", id="{44137DB3-CE47-54F0-B204-5BA4FE6C91F2}")
Monitor (primary=true,dir=none,work-area={l:0,t:0,r:1920,b:1080},dpi=(96 96),device="\\\\.\\DISPLAY1")
Monitor (primary=false,dir=none,work-area={l:-1440,t:0,r:0,b:1080},dpi=(144 144),device="\\\\.\\DISPLAY24")
...
Here indentations mark different levels in the frame tree where an object comes from. The info presented is a little dense, but most of the field names are self-explanatory. You can just play with the windows/frames, and see how their properties change.
Note that Jwno populates the frame tree lazily, so your virtual desktops may not show up in the printed info, if you never opened any window in that virtual desktop since Jwno started running.
These printed info is mostly used for troubleshooting. You should traverse the frame tree directly if you want to access its nodes programmatically.
Traversing the Frame Tree
Each node in the frame tree is a tree node object (yeah, really). All internal tree nodes have a :parent
property and a :children
property. As an example, we can inspect the root node in the REPL:
(def node (get-in jwno/context [:window-manager :root]))
(def parent (in node :parent))
(def children (in node :children))
(printf "Parent: %n, number of children: %n" parent (length children))
Since it's the root node, its :parent
is nil
. And the :children
property is a simple array, where you can have access to all its child nodes.
The tree-node-dump-subtree
function in win.janet
is a good reference for traversing the frame tree recursively (It's the implementation of the :dump-subtree
method we used to print the frame tree above).
Types of Tree Nodes
There are currently four types of nodes in the frame tree. You can check the :type
property of a node to determine its type:
(case (in node :type)
# A managed window, which has no children
:window
...
# A frame, which contains windows or other frames
:frame
...
# A virtual desktop, which contains top-level frames (monitors)
:layout
...
# The root of the frame tree, which contains layouts (virtual desktops)
:virtual-desktop-container
...
)
There's no monitor type though. In Jwno, a monitor is just a special :frame
, called a top-level frame. You can check a frame node's :monitor
property to see if it's a monitor:
(if-let [monitor (in node :monitor)]
(do
# It's a monitor
)
(do
# It's a normal frame otherwise
)
)
For all types of tree nodes, their supported methods can be inspected by peeking inside their prototypes:
(keys (table/proto-flatten node))
Window Nodes
Only managed windows have corresponding window nodes. Besides the generic tree node properties (i.e. :type
, :parent
and :children
), a window object currently only have two extra properties: :hwnd
and :tags
.
The :hwnd
property is the window's native Win32 window handle, which can be passed to low-level Win32 functions. For example, we can call ShowWindow
to hide/show a window in the REPL:
Welcome to Jwno REPL!
[127.0.0.1:9999]:1: (use jw32/_winuser)
# Put this line (without the prompt, of course) into the REPL,
# and switch to the managed window you want to manipulate while
# the REPL is sleeping
[127.0.0.1:9999]:2: (ev/sleep 3) (def win (:get-current-window (get-in jwno/context [:window-manager :root])))
# Be careful when doing this, you may lose track of the hidden
# window. Use some window you can throw away, e.g. an empty
# Notepad window. It's a fun trick to play with though 😜
[127.0.0.1:9999]:3: (ShowWindow (in win :hwnd) SW_HIDE)
# Do this to get your window back
[127.0.0.1:9999]:4: (ShowWindow (in win :hwnd) SW_SHOW)
The ev/sleep
trick is something I use when debugging Jwno itself. Given the power of the REPL, you can certainly find other ways to retrieve a window node.
The next window property we'll talk about is :tags
. It's a Janet table that stores any user-defined properties for that window. For example, you can mark your favorite windows when they're created:
(:add-hook (in jwno/context :hook-manager) :window-created
(fn [win _uia _exe _desktop]
(when (is-my-favorite-window? win)
(put (in win :tags) :favorite true))))
And then pull them up by checking their :tags
when using the :summon
command:
(:call-command (in jwno/context :command-manager)
:summon
(fn [win]
(def tags (in win :tags))
(= true (in tags :favorite))))
Note that certain tag names are used by Jwno to layout the windows, you can set them to the values you prefer, but cannot use these names for other purposes:
-
:anchor
: Used to determine where the window should be placed when it does not fill the whole frame. Can be one of:center
,:top-left
,:top
,:top-right
,:left
,:right
,:bottom-left
,:bottom
,:bottom-right
. It's only meaningful when:no-expand
or:no-resize
is specified, or when the window can not be resized. -
:forced
: Whether this window is managed forcibly. -
:frame
: A specific frame to put this window into. This overrides the default window placement rules, and can only be set in the:window-created
hook. -
:margin
and:margins
: Used to specify margins (gaps) around the window, when fitting it into a frame. You can specify non-uniform margins in:margins
like this:(put tags :margins {:left 10 :top 20 :right 30 :bottom 40})
, and they will override the value in:margin
. -
:no-expand
: Tell Jwno not to enlarge a window when putting it into a frame (but shrinking can still happen). Can be used when dealing with modal dialog boxes, so that they fit nicely inside the frame, without covering their owner windows. -
:no-resize
: Tell Jwno not to resize a window when taking it under management.
All these properties, except :frame
, take effect when retiling or transforming a window. For example, you can experiment with the :margin
setting in the REPL, like this:
(put (in win :tags) :margin 100) # Or any other value you prefer
(:call-command (in jwno/context :command-manager) :retile)
Frame Nodes
⚠️🚨🚧 UNDER CONSTRUCTION 🚧🚨⚠️