Skip to content

Commit

Permalink
WinGet: Add sub-commands PID, ProcessName, Count and List / Fix match…
Browse files Browse the repository at this point in the history
…ing all windows by specifying empty title
  • Loading branch information
phil294 committed Jul 29, 2023
1 parent ba76ea5 commit 70d0b14
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 33 deletions.
12 changes: 5 additions & 7 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14043,14 +14043,12 @@ <h2 id="Examples">Examples</h2>
<p class="calibre8"> </p>
<p class="calibre8"><em class="calibre21">Cmd</em> is the operation to perform, which if blank defaults to <em class="calibre21">ID</em>. It can be one of the following words:</p>
<p class="calibre8"><strong class="calibre14">ID</strong>: Retrieves the unique ID number for the first window matching the specified <em class="calibre21">WinTitle</em>, <em class="calibre21">WinText</em>, <em class="calibre21">ExcludeTitle</em>, and <em class="calibre21">ExcludeText</em>. If there is none, <em class="calibre21">OutputVar</em> is made blank.</p>
<div class="tbd">
<p class="calibre8"><strong class="calibre14">IDLast</strong>: Same as above except it retrieves the ID of the last/bottommost window if there is more than one match. If there is only one match, it performs identically to <em class="calibre21">ID</em>. This concept is similar to that used by <a href="#WinActivateBottom.htm" class="pcalibre3 pcalibre1 pcalibre calibre5 pcalibre2">WinActivateBottom</a>.</p>
<p class="calibre8"><strong class="calibre14">PID</strong> [v1.0.18+]: Retrieves the <a href="#Process.htm" class="pcalibre3 pcalibre1 pcalibre calibre5 pcalibre2">Process ID (PID)</a> for the first window matching the specified <em class="calibre21">WinTitle</em>, <em class="calibre21">WinText</em>, <em class="calibre21">ExcludeTitle</em>, and <em class="calibre21">ExcludeText</em>.</p>
<p class="calibre8"><strong class="calibre14">ProcessName</strong> [v1.0.22+]: Retrieves the name of the process (e.g. notepad.exe) that owns the first matching window. If there are no matching windows, <em class="calibre21">OutputVar</em> is made blank.</p>
<p class="calibre8 tbd"><strong class="calibre14">IDLast</strong>: Same as above except it retrieves the ID of the last/bottommost window if there is more than one match. If there is only one match, it performs identically to <em class="calibre21">ID</em>. This concept is similar to that used by <a href="#WinActivateBottom.htm" class="pcalibre3 pcalibre1 pcalibre calibre5 pcalibre2">WinActivateBottom</a>.</p>
<p class="calibre8"><strong class="calibre14">PID</strong>: Retrieves the <a href="#Process.htm" class="pcalibre3 pcalibre1 pcalibre calibre5 pcalibre2">Process ID (PID)</a> for the first window matching the specified <em class="calibre21">WinTitle</em>, <em class="calibre21">WinText</em>, <em class="calibre21">ExcludeTitle</em>, and <em class="calibre21">ExcludeText</em>.</p>
<p class="calibre8"><strong class="calibre14">ProcessName</strong>: Retrieves the name of the process (e.g. notepad.exe) that owns the first matching window. If there are no matching windows, <em class="calibre21">OutputVar</em> is made blank.</p>
<p class="calibre8"><strong class="calibre14">Count</strong>: Retrieves the number of matching windows that exist (0 if none).</p>
<p class="calibre8"><strong class="calibre14">List</strong>: Retrieves the unique ID numbers for all matching windows (to retrieve all windows on the entire system, leave <em class="calibre21">WinTitle</em> and <em class="calibre21">WinText</em> blank but specify Program Manager or a non-existent title for <em class="calibre21">ExcludeTitle</em>). Each ID number is stored in an <a href="#Arrays.htm" class="pcalibre3 pcalibre1 pcalibre calibre5 pcalibre2">array element</a> whose name begins with <em class="calibre21">OutputVar</em>'s own name, while <em class="calibre21">OutputVar</em> itself is set to the number of retrieved items (0 if none). For example, if <em class="calibre21">OutputVar</em> is MyArray and two matching windows are discovered, MyArray1 will be set to the ID of the first window, MyArray2 will be set to the ID of the second window, and MyArray itself will be set to the number 2.</p>
<p class="calibre8"><strong class="calibre14">MinMax</strong> [v1.0.22+]: Retrieves the minimized/maximized state for the first matching window. <em class="calibre21">OuputVar</em> is made blank if no matching window exists; otherwise, it is set to one of the following numbers:<br class="calibre12" /> -1: The window is minimized (<a href="#WinRestore.htm" class="pcalibre3 pcalibre1 pcalibre calibre5 pcalibre2">WinRestore</a> can unminimize it). <br class="calibre12" /> 1: The window is maximized (<a href="#WinRestore.htm" class="pcalibre3 pcalibre1 pcalibre calibre5 pcalibre2">WinRestore</a> can unmaximize it).<br class="calibre12" /> 0: The window is neither minimized nor maximized.</p>
</div>
<p class="calibre8"><strong class="calibre14">List</strong>: Retrieves the unique ID numbers for all matching windows (to retrieve all windows on the entire system, leave <em class="calibre21">WinTitle</em> and <em class="calibre21">WinText</em> blank but specify <span class="rm">Program Manager or</span> a non-existent title for <em class="calibre21">ExcludeTitle</em>). Each ID number is stored in an <a href="#Arrays.htm" class="pcalibre3 pcalibre1 pcalibre calibre5 pcalibre2">array element</a> whose name begins with <em class="calibre21">OutputVar</em>'s own name, while <em class="calibre21">OutputVar</em> itself is set to the number of retrieved items (0 if none). For example, if <em class="calibre21">OutputVar</em> is MyArray and two matching windows are discovered, MyArray1 will be set to the ID of the first window, MyArray2 will be set to the ID of the second window, and MyArray itself will be set to the number 2.</p>
<p class="calibre8 tbd"><strong class="calibre14">MinMax</strong> [v1.0.22+]: Retrieves the minimized/maximized state for the first matching window. <em class="calibre21">OuputVar</em> is made blank if no matching window exists; otherwise, it is set to one of the following numbers:<br class="calibre12" /> -1: The window is minimized (<a href="#WinRestore.htm" class="pcalibre3 pcalibre1 pcalibre calibre5 pcalibre2">WinRestore</a> can unminimize it). <br class="calibre12" /> 1: The window is maximized (<a href="#WinRestore.htm" class="pcalibre3 pcalibre1 pcalibre calibre5 pcalibre2">WinRestore</a> can unmaximize it).<br class="calibre12" /> 0: The window is neither minimized nor maximized.</p>
<p class="calibre8"><strong class="calibre14">ControlList</strong> Retrieves the control names for all <span class="x11">interactive</span> controls in the first window matching the specified <em class="calibre21">WinTitle</em>, <em class="calibre21">WinText</em>, <em class="calibre21">ExcludeTitle</em>, and <em class="calibre21">ExcludeText</em>. If there are no controls or the target window does not exist, <em class="calibre21">OutputVar</em> is made blank. Otherwise, each control name consists of its class name followed immediately by its sequence number, as shown by Window Spy.</p>
<p class="calibre8">Each item except the last is terminated by a linefeed (`n). To examine the individual control names one by one, use a <a href="#LoopParse.htm" class="pcalibre3 pcalibre1 pcalibre calibre5 pcalibre2">parsing loop</a> as shown in the examples section below.</p>
<p class="calibre8">Controls are sorted according to their Z-order, which is usually the same order as TAB key navigation if the window supports tabbing.</p>
Expand Down
49 changes: 33 additions & 16 deletions src/cmd/x11/window/win-get.cr
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,41 @@ class Cmd::X11::Window::WinGet < Cmd::Base
def run(thread, args)
match_conditions = args[2..]? || [] of ::String
out_var = args[0]
cmd = args[1]? || "id"
Util.match(thread, match_conditions, empty_is_last_found: true, a_is_active: true) do |win|
value = case cmd.downcase
when "id"
win.window
when "controllist"
ctrls = [] of ::String
# skip non interactive: Not quite by spec, but when using this command, you usually
# only want interactive ones, and filtering out non-interactives with AHK code is
# either very hard or very slow, specially given our current line execution speed...
# But it does add a little bit of incompatibility with Windows
thread.runner.display.at_spi &.each_descendant(thread, win, max_children: 1000, skip_non_interactive: true) do |_, _, class_NN|
ctrls << class_NN
true
cmd = (args[1]? || "id").downcase
if cmd == "count" || cmd == "list"
wins = Util.match_multiple(thread, match_conditions, empty_is_last_found: true, a_is_active: true)
case cmd
when "count"
thread.runner.set_user_var(out_var, wins.size.to_s)
when "list"
thread.runner.set_user_var(out_var, wins.size.to_s)
wins.each_with_index do |win, i|
thread.runner.set_user_var("#{out_var}#{i + 1}", win.window.to_s)
end
ctrls.join '\n'
end
thread.runner.set_user_var(out_var, value.to_s)
else
Util.match(thread, match_conditions, empty_is_last_found: true, a_is_active: true) do |win|
value = case cmd
when "id"
win.window
when "pid"
win.pid
when "processname"
`ps -p #{win.pid} -o comm=`
when "controllist"
ctrls = [] of ::String
# skip non interactive: Not quite by spec, but when using this command, you usually
# only want interactive ones, and filtering out non-interactives with AHK code is
# either very hard or very slow, specially given our current line execution speed...
# But it does add a little bit of incompatibility with Windows
thread.runner.display.at_spi &.each_descendant(thread, win, max_children: 1000, skip_non_interactive: true) do |_, _, class_NN|
ctrls << class_NN
true
end
ctrls.join '\n'
end
thread.runner.set_user_var(out_var, value.to_s)
end
end
end
end
28 changes: 19 additions & 9 deletions src/cmd/x11/window/win-util.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ class Cmd::X11::Window::Util
# param `match_conditions` is those four `[, WinTitle, WinText, ExcludeTitle, ExcludeText]`
# from the docs that are used in various window commands,
# and can consequently also be an empty array.
def self.match(thread, match_conditions, *, empty_is_last_found, a_is_active, force_detect_hidden_windows = false)
private def self.match_all(thread, match_conditions, *, empty_is_last_found, a_is_active, force_detect_hidden_windows = false)
title = match_conditions[0]? || ""
if match_conditions.size == 0 || match_conditions.all? &.empty?
raise Run::RuntimeException.new "expected window matching arguments as 'last found window' cannot be inferred here" if ! empty_is_last_found
win = thread.settings.last_found_window
wins = thread.settings.last_found_window ? [thread.settings.last_found_window.not_nil!] : [] of ::XDo::Window
elsif title.downcase == "a"
raise Run::RuntimeException.new "expected window matching arguents as 'A' for active window cannot be inferred here" if ! a_is_active
win = thread.runner.display.x_do.active_window
wins = thread.runner.display.x_do.active_window ? [thread.runner.display.x_do.active_window.not_nil!] : [] of ::XDo::Window
else
if title.starts_with?("ahk_id ")
wid = title[7..].to_u64?
raise Run::RuntimeException.new "ahk_id must be a number" if ! wid
win = XDo::Window.new(thread.runner.display.x_do.xdo_p, wid)
win = nil if ! win.name # avoids segfaults
wins = win.name ? [win] : [] of ::XDo::Window # avoids segfaults
else
text = match_conditions[1]? || ""
exclude_title = match_conditions[2]? || ""
Expand All @@ -36,29 +36,35 @@ class Cmd::X11::Window::Util
# TODO: these/name etc should all be case sensitive. Maybe double filter below? How performant is querying for .name etc?
window_class_name title[10..] # TODO: is this regex? how to make partial matches like ahk?
else
title = ".*" if title.empty?
window_name title # todo same as above / seems to be partial match but only at *start* of string
end
end.reject &.name.nil?

wins.select! do |win|
if ! exclude_title.empty?
return false if win.name.not_nil!.includes? exclude_title
next false if win.name.not_nil!.includes? exclude_title
end
if ! text.empty? || ! exclude_text.empty?
win_texts = thread.runner.display.at_spi &.get_all_texts(thread, win, include_hidden: false)
return false if ! win_texts
next false if ! win_texts
if ! text.empty?
return false if win_texts.empty? || ! win_texts.index &.includes?(text)
next false if win_texts.empty? || ! win_texts.index &.includes?(text)
end
if ! exclude_text.empty?
return false if win_texts.index &.includes?(exclude_text)
next false if win_texts.index &.includes?(exclude_text)
end
end
true
end
win = wins.first?
end
end
wins
end
# :ditto:
def self.match(thread, match_conditions, *, empty_is_last_found, a_is_active, force_detect_hidden_windows = false)
wins = self.match_all(thread, match_conditions, empty_is_last_found: empty_is_last_found, a_is_active: a_is_active, force_detect_hidden_windows: force_detect_hidden_windows)
win = wins.first?
if win
yield win
# Somehow, most (all?) window manager commands like close! or minimize!
Expand All @@ -69,6 +75,10 @@ class Cmd::X11::Window::Util
end
!!win
end
# :ditto:
def self.match_multiple(thread, match_conditions, *, empty_is_last_found, a_is_active, force_detect_hidden_windows = false)
self.match_all(thread, match_conditions, empty_is_last_found: empty_is_last_found, a_is_active: a_is_active, force_detect_hidden_windows: force_detect_hidden_windows)
end
def self.coord_relative_to_screen(thread, x, y)
loc = thread.runner.display.x_do.active_window.location
return x + loc[0].to_i, y + loc[1].to_i
Expand Down
2 changes: 1 addition & 1 deletion tests.ahk
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ ifwinnotexist, ahk_x11_test_gui, btn txt 1,banana,btn txt 2
fail_reason = gui win not exist 5
gosub fail
}
ifwinexist,,,ahk_x11_test_gui
ifwinexist, ahk_x11_test_gui,,ahk_x11_test_gui
{
fail_reason = gui win not exist 6
gosub fail
Expand Down

0 comments on commit 70d0b14

Please sign in to comment.