如何在启动时以最小化的方式运行程序?

我只想让Telegram在启动时运行,并且我已经将它添加到了启动应用程序中。关键是我希望它能最小化。有什么命令可以实现吗?

如何启动Telegram并在应用程序启动后窗口的名称是什么? - Jacob Vlijm
我使用的命令只是应用程序的路径,窗口名称为Telegram Desktop。 - nermitt
嗨Hossien,以防你更喜欢使用pid而不是窗口标题,我编辑了我的回答。 - Jacob Vlijm
@JacobVlijm 谢谢!这个方法非常高效和有用!然而,第一种方法在变量窗口名称的情况下能够完美地运行。干得好! - nermitt
1@SumeetDeshmukh 你是一个非常友善和慷慨的人。真的! - Jacob Vlijm
@JacobVlijm,它实际上对我有帮助,而且是一个非常好的回答,所以我给了奖励 :) - Sumeet Deshmukh
10个回答

如何启动一个最小化的应用程序

以最小化的方式启动应用程序需要两个命令:

  • 启动应用程序
  • 将其窗口最小化

因此,命令或脚本需要“智能”;第二个命令应该等待应用程序窗口实际出现。

启动应用程序最小化的通用解决方案

下面的脚本可以实现这一点,并可用作启动应用程序最小化的通用解决方案。只需按以下语法运行即可:

<script> <command_to_run_the_application> <window_name>

剧本

#!/usr/bin/env python3
import subprocess
import sys
import time

subprocess.Popen(["/bin/bash", "-c", sys.argv[1]])
windowname = sys.argv[2]

def read_wlist(w_name):
    try:
        l = subprocess.check_output(["wmctrl", "-l"]).decode("utf-8").splitlines()
        return [w.split()[0] for w in l if w_name in w][0]
    except (IndexError, subprocess.CalledProcessError):
        return None

t = 0
while t < 30:
    window = read_wlist(windowname)
    time.sleep(0.1)
    if window != None:
        subprocess.Popen(["xdotool", "windowminimize", window])
        break
    time.sleep(1)
    t += 1

使用方法

此脚本需要同时安装 wmctrlxdotool

sudo apt-get install wmctrl xdotool

然后:

  1. 将脚本复制到一个空文件中,保存为 startup_minimizd.py
  2. 使用(例如)gedit 命令来测试运行脚本:

    python3 /path/to/startup_minimizd.py gedit gedit
    
  3. 如果一切正常,请将该命令(适用于您的应用程序)添加到 启动应用程序

解释

  • 脚本启动应用程序,并运行您作为第一个参数给出的命令。
  • 然后,脚本使用wmctrl帮助检查窗口列表,以查找与您的第二个参数同名的窗口。
  • 如果窗口出现,则立即使用xdotool将其最小化。 为了防止无限循环,如果由于某种原因窗口可能不会出现,脚本对窗口出现的时间设定了30秒的限制。

注意

无需提及您可以同时在多个应用程序上使用该脚本,因为您在脚本外部使用参数来运行它。


编辑

通过进程ID识别窗口

如果窗口标题不确定或可变,或者窗口名称存在命名冲突的风险,使用进程ID(pid)是一种更可靠的方法。

下面的脚本基于应用程序的进程ID,即wmctrl -lpps -ef的输出。

设置几乎相同,但在这个版本中不需要窗口标题,所以运行它的命令是:

python3 /path/to/startup_minimizd.py <command_to_run_application>

就像第一个脚本一样,它需要同时使用wmctrlxdotool

脚本内容

#!/usr/bin/env python3
import subprocess
import sys
import time

command = sys.argv[1]
command_check = command.split("/")[-1]

subprocess.Popen(["/bin/bash", "-c", command])

t = 1
while t < 30:
    try:
        w_list = [l.split() for l in subprocess.check_output(["wmctrl", "-lp"]).decode("utf-8").splitlines()]
        proc = subprocess.check_output(["pgrep", "-f", command_check]).decode("utf-8").strip().split()
        match = sum([[l[0] for l in w_list if p in l] for p in proc], [])
        subprocess.Popen(["xdotool", "windowminimize", match[0]])
        break
    except (IndexError, subprocess.CalledProcessError):
        pass
    t += 1
    time.sleep(1)

关于第二个脚本的说明

虽然通常情况下第二个版本应该更可靠,但在应用程序由包装脚本启动时,命令的进程ID将与最终调用的应用程序不同。

在这种情况下,我建议使用第一个脚本。



为Steam编辑的特定版本脚本

根据评论的要求,以下是一个专门为启动Steam最小化而制作的版本。

为什么需要针对Steam的特定版本?

事实证明,Steam与“普通”应用程序有很大的不同:

  • 事实证明,Steam不仅运行一个进程ID,而是至少(在我的测试中)八个!
  • Steam在启动时会出现至少两个窗口(一个类似闪屏的窗口),但有时还会出现额外的消息窗口。
  • Steam的窗口具有pid 0,这在原始脚本中是个问题。
  • 主窗口创建后,大约一秒钟后窗口会再次提升,因此单独最小化是不够的。
这种特殊的《Steam》行为需要一个特殊版本的脚本,下面是添加的脚本。该脚本启动《Steam》,并在12秒内监视所有相应《WM_CLASS》的新窗口,检查它们是否被最小化。如果没有,脚本会确保最小化。
与原始脚本类似,这个脚本需要安装《wmctrl》和《xdotool》。

脚本内容

#!/usr/bin/env python3
import subprocess
import time

command = "steam"
subprocess.Popen(["/bin/bash", "-c", command])

def get(cmd):
    return subprocess.check_output(cmd).decode("utf-8").strip()

t = 0

while t < 12:
    try:
        w_list = [l.split()[0] for l in get(["wmctrl", "-l"]).splitlines()]
        for w in w_list:
            data = get(["xprop", "-id", w])
            if all(["Steam" in data, not "_NET_WM_STATE_HIDDEN" in data]):
                subprocess.Popen(["xdotool", "windowminimize", w])
    except (IndexError, subprocess.CalledProcessError):
        pass

    t += 1
    time.sleep(1)

使用方法

只需将其复制到一个空文件中,保存为runsteam_minimized.py
通过以下命令运行它:
python3 /path/to/runsteam_minimized.py

1哇,太棒了!我不会用except:来返回None。最好让它失败,这样你就能看到出了什么问题;否则,它可能因为各种不同原因而崩溃,并且没有提示地通过。 - fedorqui
2@fedorqui 不错,可能会出现两个例外情况:subprocess.CalledProcessError(由于有问题的 wmctrl 导致)和 IndexError(正常的异常),一分钟后会进行编辑 :)。谢谢提醒。 - Jacob Vlijm
@HosseinSoltanloo 你用什么具体命令来运行脚本? - Jacob Vlijm
@JacobVlijm 脚本运行良好,但还有一个问题需要您修复。每当我有未读消息并打开应用程序时,窗口名称会更改为类似于“Telegram(2)”,因为有两条未读消息,这样脚本就无法工作了,因为名称发生了变化。 - nermitt
@HosseinSoltanloo 可以修复,然后我们必须切换到流程名称。将发布一个编辑过的版本... - Jacob Vlijm
@HosseinSoltanloo 顺便说一下,窗口名称不需要是完整的名称,在你的情况下 Telegram 就足够了。不过我会根据 pid 发布一个编辑后的版本。 - Jacob Vlijm
对于像Steam这样在运行之前打开初始化窗口的程序,你有什么想法来修改这个脚本吗?(后者是真正应该被最小化的窗口)我尝试使用了脚本的两个版本,但无法获得期望的结果(即一旦Steam实际启动,游戏库窗口不会自动最小化到启动器)。然而,对于我尝试过的另一个应用程序,它的效果非常好(谢谢!)。 - Baku9
@J.D.Holland应该是可能的。我不使用Steam,但它是像一个闪屏窗口吗?几秒钟后会消失吗?如果我再次回到自己的系统上,我可以检查并安装它。 - Jacob Vlijm
@JacobVlijm 是的,差不多就是这样。这里有一张截图。它只会显示几秒钟,然后消失,接着真正的应用程序会出现(但不会最小化自己)。可能是我操作不正确。 - Baku9
2@J.D.Holland 我相信它可以修好的。我会在接下来的几天里查看一下 :) - Jacob Vlijm
@J.D.Holland 完成了,已添加了Steam版本。Steam的表现非常出色 :) - Jacob Vlijm
@JacobVlijm 刚试了一下,基本上完全符合我的期望。我尝试了几次注销并重新登录,其中一次注销尝试时我的桌面卡住了一会儿(甚至不确定这是否与我们所做的事情有关,只是觉得值得一提)。除了那个小插曲之外,它似乎完美地运行着!感谢你在这方面付出额外的努力,你已经赢得了点赞和绿色勾选标记。:] - Baku9
太棒了!我用它来在我的服务器上启动最小化的Spotify,效果很棒。就是使用PID方法。 - Peterdk
Steam版本也可以与Franz一起使用。 - janot
仍然在2022年起作用 - Saminda Peramuna
Ubuntu 22.04的脚本失败了,显示GPU无法使用的消息。我在subprocess.Popen(["/bin/bash", "-c", command])中添加了' --no-sandbox',现在它可以正常工作了。谢谢! - undefined

拥有由用户72216和Sergey提供的脚本作为问题的一般解决方案是很好的,但有时您希望启动的应用程序已经有了一个可以实现您想要的功能的开关。

以下是一些示例及其相应的启动程序命令字符串:

  • Telegram(自版本0.7.10起)具有-startintray选项:<path-to-Telegram>/Telegram -startintray
  • Steam具有-silent选项:/usr/bin/steam %U -silent
  • Transmission具有--minimized选项:/usr/bin/transmission-gtk --minimized

在Unity中,这些应用程序会作为图标以最小化状态启动在顶部菜单栏上,而不是作为launcher上的图标,尽管一旦开始使用应用程序,正常启动图标仍将出现。其他应用程序可能行为有所不同。


1Signal有--use-tray-icon--start-in-tray选项。 - Márcio

我需要将程序关闭到托盘,而不是最小化,并且我尝试了这里发布的所有脚本,那些有效的只对某些程序有效,对其他程序无效。所以我编写了一个更好的脚本(几乎看不到窗口出现,只有托盘图标,看起来很原生),并且对我尝试过的所有程序都有效。它基于Jacob的脚本。使用这个脚本时,你可能需要根据程序添加一个参数(见下文),但对我来说总是有效的,适用于许多程序,也应该适用于Steam。
使用方法:
  1. sudo apt-get install wmctrl xdotool
  2. 将脚本保存为startup_closed.py,赋予执行权限,然后执行python3 ./startup_closed.py -c <打开程序的命令>
  3. 如果程序的托盘图标或窗口没有显示出来,则需要尝试添加以下参数之一:-splash-hide。例如:python3 ./startup_closed.py -hide -c teamviewerpython3 ./startup_closed.py -splash -c slack
  4. 还有更多的参数,但你可能不需要它们。关于参数何时以及为什么需要使用的详细信息,请参考帮助文档:./startup_closed.py --help

脚本:

#!/usr/bin/env python3
import subprocess
import sys
import time
import argparse
import random

parser = argparse.ArgumentParser(description='This script executes a command you specify and closes or hides the window/s that opens from it, leaving only the tray icon. Useful to "open closed to tray" a program. If the program does not have a tray icon then it just gets closed. There is no magic solution to achieve this that works for all the programs, so you may need to tweek a couple of arguments to make it work for your program, a couple of trial and error may be required with the arguments -splash and -hide, you probably will not need the others.')

parser.add_argument("-c", type=str, help="The command to open your program. This parameter is required.", required=True)
parser.add_argument("-splash", help="Does not close the first screen detected. Closes the second window detected. Use in programs that opens an independent splash screen. Otherwise the splash screen gets closed and the program cannot start.", action='store_true', default=False)
parser.add_argument("-hide", help="Hides instead of closing, for you is the same but some programs needs this for the tray icon to appear.", action='store_true', default=False)
parser.add_argument("-skip", type=int, default=0, help='Skips the ammount of windows specified. For example if you set -skip 2 then the first 2 windows that appear from the program will not be affected, use it in programs that opens multiple screens and not all must be closed. The -splash argument just increments by 1 this argument.', required=False)
parser.add_argument("-repeat", type=int, default=1, help='The amount of times the window will be closed or hidden. Default = 1. Use it for programs that opens multiple windows to be closed or hidden.', required=False)
parser.add_argument("-delay", type=float, default=10, help="Delay in seconds to wait before running the application, useful at boot to not choke the computer. Default = 10", required=False)
parser.add_argument("-speed", type=float, default=0.02, help="Delay in seconds to wait between closing attempts, multiple frequent attempts are required because the application may be still loading Default = 0.02", required=False)

args = parser.parse_args()

if args.delay > 0:
    finalWaitTime = random.randint(args.delay, args.delay * 2);
    print(str(args.delay) + " seconds of delay configured, will wait for: " + str(finalWaitTime))
    time.sleep(finalWaitTime)
    print("waiting finished, running the application command...")

command_check = args.c.split("/")[-1]
subprocess.Popen(["/bin/bash", "-c", args.c])

hasIndependentSplashScreen = args.splash
onlyHide = args.hide
skip = args.skip
repeatAmmount = args.repeat
speed = args.speed

actionsPerformed = 0
lastWindowId = 0

if hasIndependentSplashScreen:
    skip += 1

while True:
    try:
        w_list = [l.split() for l in subprocess.check_output(["wmctrl", "-lp"]).decode("utf-8").splitlines()]
        proc = subprocess.check_output(["pgrep", "-f", command_check]).decode("utf-8").strip().split()
        match = sum([[l[0] for l in w_list if p in l] for p in proc], [])
        if len(match) > 0:
            windowId = match[0]
            if windowId != lastWindowId:
                if skip > 0:
                    skip -= 1
                    print("skipped window: " + windowId)
                    lastWindowId = windowId
                else:
                    print("new window detected: " + windowId)
                    if onlyHide:
                        subprocess.Popen(["xdotool", "windowunmap", windowId])
                        print("window was hidden: " + windowId)
                    else:
                        subprocess.Popen(["xdotool", "key", windowId, "alt+F4"])
                        print("window was closed: " + windowId)

                    actionsPerformed += 1
                    lastWindowId = windowId

            if actionsPerformed == repeatAmmount:
                break

    except (IndexError, subprocess.CalledProcessError):
        break

    time.sleep(speed)

print("finished")

这很不错。然而,我在使用 Zoom 应用程序时遇到了一个问题,即 startup_closed.py 尝试关闭托管终端窗口,并成功关闭了我也在运行 startup_closed.py 的 vscode 窗口。不确定是因为 Zoom 应用程序已经在运行,还是其他原因。肯定是低延迟值触发了这个问题。您是否有 GitHub 或其他公共存储库可以提供? - Jason Harrison

如果程序被关闭到托盘,有些人可能实际上希望在启动时关闭程序窗口而不是最小化它。一个例子就是Viber这个程序。在这种情况下,可以使用以下脚本start_closed.sh
#!/bin/bash

# Check that there is only one input argument
if [[ $# -gt 1 ]]; then
echo "Usage: $0 <program-to-start>"
exit 1
fi

$1 &                               # Start program passed in first argument
pid=$!                             # Get PID of last started program
xdotool search --sync --pid $pid | # Wait for window with PID to appear...
xargs wmctrl -i -c                 # ...and close it

用法: <脚本路径> <要启动的程序>

1你可能需要注意,在使用Wayland安装的情况下,xdotool可能无法正常工作。 - Videonauth
我成功地完成了这个任务,而且没有使用wmctrl(它对我来说甚至都不起作用)。我使用了以下命令:xdotool search --onlyvisible --pid $pid --sync | xargs xdotool windowminimize - AlonL

我提供了一个相当优雅的解决方案,完全依赖于xdotool,对于那些没有"启动最小化"参数的应用程序(如Telegram)非常有用。
唯一的缺点是每个应用程序都需要手动制作解决方案,但假设这不是问题(例如:如果您想要在登录后自动启动某个应用程序而又不希望它在屏幕上占据空间),这种方法更简单直接。
实际示例:
## Starts Telegram and immediately closes it
xdotool search --sync --onlyvisible --name '^Telegram$' windowclose &
telegram-desktop &

## Starts WhatsApp and immediately closes it
xdotool search --sync --onlyvisible --name '(\([0-9]*\) ){0,1}(WhatsApp$|WhatsApp Web$)' windowclose &
whatsapp-nativefier &

解决方案

乍一看,您可能认为最好使用进程的 PID 或类进行匹配,但实际上这是适得其反的,因为您经常会得到相同 PID 的多个结果。例如,一个等待通知的0x0窗口,系统托盘图标或任何其他“隐藏”的窗口。

解决方案是制作一个 xdotool 命令,始终只返回一个唯一的窗口。在我的两个示例中,使用了 --name,但您可以结合多个选择器使用 --all (例如:匹配给定的 classname + 一个 class name + 一个名称正则表达式)。通常一个好的 --name 正则表达式就行了。

制作好您的 search 条件后,只需使用 --sync 参数和您的条件生成一个 xdotool 实例 (从 shell 中分离),然后跟随 windowclose。之后再运行您的应用程序:

xdotool search --sync [... myapp-match-conditions] windowclose &
my-app

查看xdotool search --help以了解您可以组合的所有可能性,以便能够定位到您想要的确切窗口。有时候会变得棘手,您需要结合几个条件,但一旦完成,它很少会失败(除非更新更改了应用程序并破坏了您的实现,当然)。

我拿了Jacob的脚本,并稍微修改了一下,使它更加通用。
#!/usr/bin/python

import os
import subprocess
import sys
import time
import signal

WAIT_TIME = 10


def check_exist(name):
    return subprocess.Popen("which "+name,
                            shell=True,
                            stdout=subprocess.PIPE
                            ).stdout.read().rstrip("-n")


def killpid(pidlist):
    for pid in pidlist:
        args = ["xdotool",
                "search",
                "--any",
                "--pid",
                pid,
                "--name",
                "notarealprogramname",
                "windowunmap",
                "--sync",
                "%@"]
        subprocess.Popen(args)


def killname(name):
    args = ["xdotool",
            "search",
            "--any",
            "--name",
            "--class",
            "--classname",
            name,
            "windowunmap",
            "--sync",
            "%@"]
    subprocess.Popen(args)


sys.argv.pop(0)

if check_exist(sys.argv[0]) == "":
    sys.exit(1)
if check_exist("xdotool") == "":
    sys.stderr.write("xdotool is not installed\n")
    sys.exit(1)
if check_exist("wmctrl") == "":
    sys.stderr.write("wmctrl is not installed\n")
    sys.exit(1)

try:
    prog = subprocess.Popen(sys.argv, preexec_fn=os.setsid)
except OSError, e:
    sys.exit(1)

time.sleep(WAIT_TIME)
idlist = subprocess.Popen("pgrep -g " + str(prog.pid),
                          shell=True,
                          stdout=subprocess.PIPE
                          ).stdout.read().splitlines()

ps1 = os.fork()
if ps1 > 0:
    ps2 = os.fork()

if ps1 == 0:  # Child 1
    os.setpgid(os.getpid(), os.getpid())
    killpid(idlist)
    sys.exit(0)
elif ps2 == 0:  # Child 2
    killname(os.path.basename(sys.argv[0]))
    sys.exit(0)
elif ps1 > 0 and ps2 > 0:  # Parent
    time.sleep(WAIT_TIME)
    os.killpg(os.getpgid(int(ps1)), signal.SIGTERM)
    os.kill(ps2, signal.SIGTERM)
    os.waitpid(ps1, 0)
    os.waitpid(ps2, 0)
    sys.exit(0)
else:
    exit(1)

主要的区别如下:
  • 程序为进程设置了组ID(GID)。因此,所有子进程及其窗口可以很容易地找到。
  • 使用xdotool的--sync选项,而不是使用while循环。
  • 脚本允许将参数传递给程序。
WAIT_TIME应设置足够大,以允许程序分叉其子进程。在我的电脑上,这对于像Steam这样的大型程序已经足够了。如果需要,可以增加它。 补充说明 xdotoolwindowunmap选项可能与某些应用程序和托盘程序(例如Linux Mint的托盘)一起工作不正常,因此这里提供了一个针对这些异常情况的替代版本的脚本。
#!/usr/bin/python

import os
import subprocess
import sys
import time
import signal

WAIT_TIME = 10


def check_exist(name):
    return subprocess.Popen("which "+name,
                            shell=True,
                            stdout=subprocess.PIPE
                            ).stdout.read().rstrip("-n")


def killpid(pidlist):
    for pid in pidlist:
        args = ["xdotool",
                "search",
                "--sync",
                "--pid",
                pid]
        for i in subprocess.Popen(args,
                                  stdout=subprocess.PIPE).\
                stdout.read().splitlines():
            if i != "":
                subprocess.Popen("wmctrl -i -c " +
                                 hex(int(i)), shell=True)


def killname(name):
    args = ["xdotool",
            "search",
            "--sync",
            "--any",
            "--name",
            "--class",
            "--classname",
            name]
    for i in subprocess.Popen(args,
                              preexec_fn=os.setsid,
                              stdout=subprocess.PIPE)\
            .stdout.read().splitlines():
        if i != "":
            subprocess.Popen("wmctrl -i -c " + hex(int(i)),
                             shell=True)


sys.argv.pop(0)

if check_exist(sys.argv[0]) == "":
    sys.exit(1)
if check_exist("xdotool") == "":
    sys.stderr.write("xdotool is not installed\n")
    sys.exit(1)
if check_exist("wmctrl") == "":
    sys.stderr.write("wmctrl is not installed\n")
    sys.exit(1)


try:
    prog = subprocess.Popen(sys.argv, preexec_fn=os.setsid)
except OSError, e:
    sys.exit(1)

time.sleep(WAIT_TIME)
idlist = subprocess.Popen("pgrep -g " + str(prog.pid),
                          shell=True,
                          stdout=subprocess.PIPE
                          ).stdout.read().splitlines()

ps1 = os.fork()
if ps1 > 0:
    ps2 = os.fork()

if ps1 == 0:  # Child 1
    os.setpgid(os.getpid(), os.getpid())
    killpid(idlist)
    sys.exit(0)
elif ps2 == 0:  # Child 2
    killname(os.path.basename(sys.argv[0]))
    sys.exit(0)
elif ps1 > 0 and ps2 > 0:  # Parent
    time.sleep(WAIT_TIME)
    os.killpg(os.getpgid(int(ps1)), signal.SIGTERM)
    os.kill(ps2, signal.SIGTERM)
    os.waitpid(ps1, 0)
    os.waitpid(ps2, 0)
    sys.exit(0)
else:
    exit(1)

我试了你的第一个脚本。它要么不起作用,要么最小化得不够快。我将其保存为startminimized。然后我运行了startminimized gnome-calendar。日历打开并继续运行吗? - Khurshid Alam
1你可以尝试增加变量WAIT_TIME。对于性能较差的电脑,我使用40秒的延迟。另外,你也可以尝试第二个脚本,因为它使用了不同的命令来最小化应用程序。 - Sergey

我刚刚在浏览中看到这个问题,所以我想知道你使用的是什么操作系统? 至于我,我正在使用UBUNTU BUDGIE 18.04 LTS,在这个操作系统中非常简单。
只需进入菜单

从菜单中选择Budgie桌面设置

然后

从桌面设置中选择自动启动

它会给你两个选项,通过"+"添加

1. 添加应用程序

2. 添加命令

通过选择添加应用程序,所有应用程序都将列出,选择任何你想要的应用程序,它将在你启动电脑时自动启动,并且也会最小化显示。

在命令后面添加&可以完成任务 <运行应用程序的命令> &

只需在启动命令中添加-u
例如,要最小化运行Slack,请使用以下命令:
/usr/bin/slack %U -u

在KDE 5中,自动启动位于“系统设置 - 启动和关机 - 自动启动”中。

看了这个帖子中的其他回答,你觉得你的回答可能是不完整的(即不适用于所有应用)并且缺乏一些解释吗? - Jeremy
添加了一个示例。 - Psijic

类似于另一个答案,但只使用一行代码和启动脚本(菜单>启动应用程序)。wmctrl -c <WIN>通过将字符串与窗口标题匹配来自然关闭窗口。例如,下面我关闭了Autokey、Telegram、Pidgin和Whatsapp这些程序:
/bin/bash -c "sleep 25 && wmctrl -c 'Autokey' && wmctrl -c 'Telegram' && wmctrl -c 'Buddy' && wmctrl -c 'Whatsapp'"