View Javadoc
1   /*
2    * Copyright 2021-2022 The OSHI Project Contributors
3    * SPDX-License-Identifier: MIT
4    */
5   package oshi.driver.unix;
6   
7   import java.awt.Rectangle;
8   import java.util.ArrayList;
9   import java.util.HashMap;
10  import java.util.LinkedHashMap;
11  import java.util.List;
12  import java.util.Map;
13  import java.util.Map.Entry;
14  import java.util.regex.Matcher;
15  import java.util.regex.Pattern;
16  
17  import oshi.annotation.concurrent.ThreadSafe;
18  import oshi.software.os.OSDesktopWindow;
19  import oshi.util.ExecutingCommand;
20  import oshi.util.ParseUtil;
21  import oshi.util.Util;
22  
23  /**
24   * Utility to query X11 windows
25   */
26  @ThreadSafe
27  public final class Xwininfo {
28  
29      private static final String[] NET_CLIENT_LIST_STACKING = ParseUtil.whitespaces
30              .split("xprop -root _NET_CLIENT_LIST_STACKING");
31      private static final String[] XWININFO_ROOT_TREE = ParseUtil.whitespaces.split("xwininfo -root -tree");
32      private static final String[] XPROP_NET_WM_PID_ID = ParseUtil.whitespaces.split("xprop _NET_WM_PID -id");
33  
34      private Xwininfo() {
35      }
36  
37      /**
38       * Gets windows on the operating system's GUI desktop.
39       *
40       * @param visibleOnly Whether to restrict the list to only windows visible to the user.
41       * @return A list of {@link oshi.software.os.OSDesktopWindow} objects representing the desktop windows.
42       */
43      public static List<OSDesktopWindow> queryXWindows(boolean visibleOnly) {
44          // Attempted to implement using native X11 code. However, this produced native X
45          // errors (e.g., BadValue) which cannot be caught on the Java side and
46          // terminated the thread. Using x command lines which execute in a separate
47          // process. Errors are caught by the terminal process and safely ignored.
48  
49          // Get visible windows in their Z order. Assign 1 to bottom and increment.
50          // All other non visible windows will be assigned 0.
51          Map<String, Integer> zOrderMap = new HashMap<>();
52          int z = 0;
53  
54          // X commands don't work with LC_ALL
55          List<String> stacking = ExecutingCommand.runNative(NET_CLIENT_LIST_STACKING, null);
56          if (!stacking.isEmpty()) {
57              String stack = stacking.get(0);
58              int bottom = stack.indexOf("0x");
59              if (bottom >= 0) {
60                  for (String id : stack.substring(bottom).split(", ")) {
61                      zOrderMap.put(id, ++z);
62                  }
63              }
64          }
65          // Get all windows along with title and path info
66          Pattern windowPattern = Pattern.compile(
67                  "(0x\\S+) (?:\"(.+)\")?.*: \\((?:\"(.+)\" \".+\")?\\)  (\\d+)x(\\d+)\\+.+  \\+(-?\\d+)\\+(-?\\d+)");
68          Map<String, String> windowNameMap = new HashMap<>();
69          Map<String, String> windowPathMap = new HashMap<>();
70          // This map will include all the windows, preserve the insertion order
71          Map<String, Rectangle> windowMap = new LinkedHashMap<>();
72          // X commands don't work with LC_ALL
73          for (String line : ExecutingCommand.runNative(XWININFO_ROOT_TREE, null)) {
74              Matcher m = windowPattern.matcher(line.trim());
75              if (m.matches()) {
76                  String id = m.group(1);
77                  if (!visibleOnly || zOrderMap.containsKey(id)) {
78                      String windowName = m.group(2);
79                      if (!Util.isBlank(windowName)) {
80                          windowNameMap.put(id, windowName);
81                      }
82                      String windowPath = m.group(3);
83                      if (!Util.isBlank(windowPath)) {
84                          windowPathMap.put(id, windowPath);
85                      }
86                      windowMap.put(id, new Rectangle(ParseUtil.parseIntOrDefault(m.group(6), 0),
87                              ParseUtil.parseIntOrDefault(m.group(7), 0), ParseUtil.parseIntOrDefault(m.group(4), 0),
88                              ParseUtil.parseIntOrDefault(m.group(5), 0)));
89                  }
90              }
91          }
92          // Get info for each window
93          // Prepare a list to return
94          List<OSDesktopWindow> windowList = new ArrayList<>();
95          for (Entry<String, Rectangle> e : windowMap.entrySet()) {
96              String id = e.getKey();
97              long pid = queryPidFromId(id);
98              boolean visible = zOrderMap.containsKey(id);
99              windowList.add(new OSDesktopWindow(ParseUtil.hexStringToLong(id, 0L), windowNameMap.getOrDefault(id, ""),
100                     windowPathMap.getOrDefault(id, ""), e.getValue(), pid, zOrderMap.getOrDefault(id, 0), visible));
101         }
102         return windowList;
103     }
104 
105     private static long queryPidFromId(String id) {
106         // X commands don't work with LC_ALL
107         String[] cmd = new String[XPROP_NET_WM_PID_ID.length + 1];
108         System.arraycopy(XPROP_NET_WM_PID_ID, 0, cmd, 0, XPROP_NET_WM_PID_ID.length);
109         cmd[XPROP_NET_WM_PID_ID.length] = id;
110         List<String> pidStr = ExecutingCommand.runNative(cmd, null);
111         if (pidStr.isEmpty()) {
112             return 0;
113         }
114         return ParseUtil.getFirstIntValue(pidStr.get(0));
115     }
116 
117 }