P1_ProjectLauncher

Le 六 07 三月 2026

本篇为个人工具项目ProjectLauncher的介绍,如果感兴趣,可以直接复制代码,整个项目总共为两个文件,并且只是为了文件看起来好看而分开,实际可以放置于统一文件中。

ProjectLauncher,从名字就可以看出来,用于启动项目,存在的原因很简单,我是一个以终端作为中心的工作流,而这是我的一个项目的路径:/run/media/qingyu/Making/Arukas_Java/src/main/java/com/qingyu。并且为了调试方便,我会开两个终端,运行位于Arukas_Java中的Gradle脚本。而这个时候就很难受了,需要cd进这两个目录,然后再启动Nvim或者输入运行指令。像这样的目录有很多,而且我是个相当三心二意的人,我会同时开非常多的大小项目,于是,我就随便写了这个ProjectLauncher,用于比较统一的管理项目,以及对开始工作前的初始化工作进行自动化。

这个项目的需求就是,以终端为核心(如果你习惯使用图形界面,这个项目对你来说没那么有用),自动执行,允许多窗口,允许自定义。所以我开发了一个非常简单的流程控制脚本用作配置。

大致的使用方法如下:

pl init <ProjectName> --初始化当前目录为工作目录
nvim INIT.plc --进行流程文件的编写,我使用的是nvim,你可以随便用别的编辑器
pl run <ProjectName> --运行名称为<ProjectName>的项目

以上为初始化一个目录为工作目录,然后使用的流程,你可以使用help命令来查看其他命令。整个工具运行,主要依赖于INIT.plc中的控制脚本。比如我上面所提到的项目的控制脚本如下:

  set editor nvim
  cmd 0
0 goto /run/media/qingyu/Making/Arukas_Java/src/main/java/com/qingyu
0 editor Arukas.java
  cmd 1
1 goto .
1 type ./gradlew run

它看起来稍微有点像汇编语言,符合两种可能的结构:指令+参数(s)操作目标+指令+参数(s),这里的(s)指参数可能有多个。接下来我会介绍你应该如何编写一个这样的脚本。

通常在开头,或者启动一个终端前,你应该设置你将要使用的编辑器命令,像这样:

  set editor <EditorName>

需要注意的是,如果你的结构符合指令+参数,你需要在指令前添加两个空格(Space),否则指令分割工作会不正常。以及,必须将第二个参数写为“editor”,实际上这是一个非常简易的变量系统,只是我们硬编码了editor作为预留,并且没有实现获取变量值这个功能。

接着,你需要启动终端:

  cmd <Num>

这条命令会启动一个终端,并将其编号为可以为任意不重复的数。为了演示,我们将其填0。

然后你就可以开始构建流程了,这里是你可以使用的一些指令: | Command | Usage | | -------------- | -------------------------------------------------- | | goto | 切换至传入的目录 | | editor | 启动设置的编辑器,打开传入的文件 | | type | 将指令后所有字符作为内容输入终端,被解读为回车 | 这些指令都需要依照<操作目标> <指令> <参数(s)>的结构进行编写,操作目标为你希望操作的终端的编号,比如操作终端0则写0,没有顺序要求,只要编号存在即可。

这些指令应该足够大多数场景使用,而有两个特殊指令,flagstartflagend,用于简单的选择执行。比如这个例子:

  set editor nvim
  cmd 0
0 goto .
0 editor init.lua
  flagstart plugins
  cmd 1
1 goto /home/qingyu/.config/nvim/lua/config
1 editor lazy.lua
  flagend

在这个脚本中,如果使用pl run 运行,你只会得到一个终端,而只有在使用pl run --plugins时,它才会启动第二个终端。显而易见,包裹在flagstart 与flagend 之间的内容默认不执行,只有在附加flagstart后的flag后,才会运行。简单来说,你可以通过flagstart 来设置一段流程只有在收到flag时才执行。只要在运行的命令后,加上--(这里的两个横杠(--)是必须的),相关段就可以执行。以及,flagstart指令是可以嵌套的,具体的行为可以自己测试.如果有多个flagstart,只要传入多个flag即可,比如这样:pl run --FLAG1 --FLAG2

以上为大致的编写流程。在进入使用流程前,请注意,这个工具并不是严格的按照防御性编程策略进行开发的,所以格式要求非常严格,并且实际上,每一个指令的传参数量都没有限制,只是没有使用多余的参数而已,但是依旧不建议你传入过多参数,我也不知道会发生什么。以及,这个工具完全是为了配合我的工作方式而设计的,所以只有一些参考作用,甚至根本就只是个人的项目展示。如果你真的很想试试,你需要使用X Server作为窗口系统,并且以下代码有需要修改的部分,比如cmd指令的实现,我们通过Ctrl+alt+T快捷键调起终端,你可能需要自己修改: 首先你需要安装xdotool,你可以用apt、dnf等包管理器安装它。只要你在终端中使用xdotool命令,显示的是命令列表,那就是安装成功了。 接着需要创建三个文件,"main.py"、"parser.py"、"WorkSpaces.txt"。然后在main.py中输入以下内容:

from parser import ParseCommand
from sys import exit,argv
from os import getcwd,listdir,remove,mkdir
from os.path import exists

def CheckArgv(Index:int,msg:str):
    try:
        argv[Index]
    except:
        print(msg)
        exit(1)

def ReadWorkSpaces():
    f=open(argv[0].replace("main.py","")+"WorkSpaces.txt","r",encoding="utf-8")
    Raw=f.readlines()
    f.close()
    Name=[]
    Path=[]
    for line in Raw:
        Name.append(line.split(" ")[0])
        Path.append(line.split(" ")[1].replace("\n",""))

    return dict(zip(Name,Path))


CheckArgv(1,"Usage:\n  pl <command> [options]\nCommands:\n  init         Initialize current floder as workspace.\n  run          open workspace.\n  delete       Delete a workspace\n  list         list usable workspaces\n  new          create new workspace")

if argv[1] == "init":
    CheckArgv(2,"Workpace must have a name!")
    path=getcwd()
    if "INIT.plc" in listdir(path):
        print("This folder was already initialized!")
        exit(0)
    f=open(path+"/INIT.plc","w")
    f.close()
    f1=open(argv[0].replace("main.py","")+"WorkSpaces.txt","a",encoding="utf-8")
    f1.write(argv[2]+" "+path+"\n")
    f1.close()
elif argv[1] == "run":
    CheckArgv(2,"Lost workspace name!")
    Workspaces=ReadWorkSpaces()
    if argv[2] in Workspaces.keys():
        ParseCommand(Workspaces[argv[2]]+"/INIT.plc",argv[2],argv[2:])
    else:
        print("WorkSpace "+argv[2]+" not found!")
elif argv[1] == "delete":
    CheckArgv(2,"Lost delete workspace name!")
    if not argv[2] in ReadWorkSpaces():
        print("Can't found workspace:"+argv[2])
        exit(1)
    print("This operate WON'T delete your workspace folder,if you want delete your folder,please do it mannualy.")
    if input("Are you sure you want to delete workspace "+argv[2]+"? (Y/n)").upper() == "Y":
        try:
            Path=ReadWorkSpaces()[argv[2]]
            if exists(Path+"/INIT.plc"):
                remove(Path+"/INIT.plc")
            print("Success delete "+argv[2]+".")
        except:
            print("Fail to remove INIT.plc file in "+Path+" you can try to do this mannualy.")
            exit(1)
        f2=open(argv[0].replace("main.py","")+"WorkSpaces.txt","r",encoding="utf-8")
        RawWork=f2.readlines()
        f2.close()
        f3=open(argv[0].replace("main.py","")+"WorkSpaces.txt","w",encoding="utf-8")
        for i in RawWork:
            if i.split(" ")[0] != argv[2]:
                f3.write(i)
elif argv[1] == "list":
    print("Name                     Path")
    for i in ReadWorkSpaces().keys():
        out=i.ljust(25," ")+ReadWorkSpaces()[i]
        print(out)
elif argv[1] == "new":
    CheckArgv(2,"WorkSpace must have a name!")
    try:
        mkdir(argv[2])
        print("Created folder "+argv[2]+"!")
    except FileExistsError:
        print("The folder is exists!")
        exit(1)
    try:
        path=getcwd()+"/"+argv[2]
        f=open(path+"/INIT.plc","w")
        f.close()
        f1=open(argv[0].replace("main.py","")+"WorkSpaces.txt","a",encoding="utf-8")
        f1.write(argv[2]+" "+path+"\n")
        f1.close()
    except:
        print("can't create INIT.plc!You can try to do this mannualy.")
        exit(1)
else:
    print("Unknown command "+argv[1])

然后在parser.py中输入以下内容。:

from sys import exit
from os.path import exists
from os import system
from time import sleep

def CheckArgsAmount(Command:list,Amount:int):
    return len(Command)>=Amount


def ParseCommand(Path:str,PrjName:str,FLAGs:list):
    f=open(Path,"r",encoding="utf-8")
    Raw=f.readlines()
    f.close()
    UseableCommand=["goto","editor","set","cmd","type"]
    Vars={}
    Terminals=[]
    N_FLAG=["Normal"]#You can think this as a stack
    for flag in FLAGs:
        FLAGs[FLAGs.index(flag)] = flag.replace("--","")
    for line in Raw:
        line=line.replace("\n","")
        if line[0]==" ":
            Command=line.split(" ")[2:]
        else:
            Command=line.split(" ")
        if not CheckArgsAmount(Command,2) and Command[0]!="flagend":
            print("Invaild syntax in line:"+line)
            exit(1)
        print(Command)
        if Command[0] == "cmd":
            sleep(0.7)

        if Command[0] == "flagstart":
            N_FLAG.append(Command[1])#Push the FLAG into the stack
            continue
        if Command[0] == "flagend":
            del N_FLAG[-1]#Pop out the last FLAG
            continue
        if N_FLAG[-1] in FLAGs or N_FLAG[-1]=="Normal":
            if not Command[0].isdigit() and Command[0] in UseableCommand:
                if Command[0]=="set":
                    if CheckArgsAmount(Command,3):
                        Vars[Command[1]]=Command[2]
                    else:
                        print("Command 'set' need at least 2 agrs!")
                        exit(1)
                if Command[0]=="cmd":
                    if not CheckArgsAmount(Command,2):
                        print("Command 'cmd' need at least 1 args!")
                        exit(1)
                    else:
                        system("xdotool key ctrl+alt+t")
                        sleep(0.7)
                        system("xdotool key ctrl+shift+s && xdotool type '"+PrjName+"-"+Command[1]+"' && xdotool key Return")
                        Terminals.append(Command[1])
            elif Command[0].isdigit() and Command[1] in UseableCommand:
                if Command[1]=="goto":
                    if not CheckArgsAmount(Command,3):
                        print("Command 'goto' need at least 1 agrs!")
                        exit(1)
                    if Command[2]==".":
                        Command[2]=Path.replace("/INIT.plc","")
                    if not exists(Command[2]):
                        print("Path '"+Command[2]+"' not exist!")
                        exit(1)
                    if not Command[0] in Terminals:
                        print("Can't found Terminal"+Command[0])
                        exit(1)
                    system("xdotool search '"+PrjName+"-"+Command[0]+"' windowactivate && xdotool type 'cd "+Command[2]+"' && xdotool key Return")
                if Command[1]=="editor":
                    try:
                        system("xdotool type '"+Vars['editor']+" "+Command[2]+"' && xdotool key Return")
                    except:
                        print("Var editor not found!")
                        exit(1)
                if Command[1]=="type":
                    if not CheckArgsAmount(Command,3):
                        print("Command 'type' need at least 1 args!")
                        exit(1)
                    system("xdotool search '"+PrjName+"-"+Command[0]+"' windowactivate && sleep .2 && xdotool type '"+" ".join(Command[2:]).replace("<CR>","")+"'")
                    if "<CR>" in "".join(Command[2:]):
                        system("xdotool search '"+PrjName+"-"+Command[0]+"' windowactivate && sleep .2 && xdotool key Return")
                if Command[1]=="key":
                    if not CheckArgsAmount(Command,3):
                        print("Command 'key' need at least 1 args!")
                        exit(1)
                    system("xdotool search '"+PrjName+"-"+Command[0]+"' windowactivate && sleep .2 && xdotool key "+Command[2])

            else:
                print("Unknow command in line:"+line)
                exit(1)

最后,在你的"~/.bashrc"的末尾添加如下内容,其中的python字段请自行修改为你的python命令

pl() {
    python /run/media/qingyu/Making/ProjectLauncher/main.py $@
}

然后你应该可以在新的终端中使用pl指令了。我建议首先使用help指令查看用法。 最后,我还是想说明,这个项目只是一个个人工具,我个人虽然用的很好,但是对于不熟悉的人,可能用的很难受,所以更多是作为交流用,如果产生了什么不良影响,本人概不负责,感谢。

Par QingYu, Catégorie : misc

Tags : Proggraming / Personal-Projects /