.ssh/config 里面内容太多,用 Python fiet 做了个带 ui 的检索工具

2023-11-19 20:04:23 +08:00
 yqf0215

之前用的 python tk 做的,但苹果系统升级到新版后,在点击的处理上,有些问题。

程序的作用是,自动解析 .ssh/config 文件,解析出 host 单击 标题头 可以排序 单击行, 会复制相应的 ssh your_host_config 到剪贴板; TODO: 右键单击,会 Term 中运行 ssh your_host_config ,未实现,用于右键单击后,能自动打开 term 并运行 ssh your_host_config

为什么要做这个呢? 因为我的需求,是 host 很多,而有时候并不能完全记得 host 的关键词是什么,所以需要需要一些补充信息。 ssh config 文件中,以空白行来分割不同的 Host 段落,每个段落前面可以有一行“#tags niit,南京” 这个标签 tags 就是是我用来补充信息的,host 是 niit ,tags 是南京工业职业技术大学,可能一段时间之后,我只想得起 职业 两个字。我需要一个检索 ssh Host 的方法,输入我能想起来的关键字,能找到 niit 这个 Host 。

flet 的问题:想给输入框加个 ctrl+w 的快捷键,加不了;想加个双击事件,也不支持。 只有 listview ,没有 gridview 。 所以界面格式不是表格的格式,是个 listview 。

原来帖子在: https://v2ex.com/t/863264

新版本用了 flet ,基于 flutter 的 Python ui 库,这下好用多了。只是启动速度略微有点慢,要 2/3 秒的停顿。但依然很快。

代码如下:

from tkinter import *
from tkinter.ttk import *
import pyperclip3
import sys

import flet as ft
from flet import TextField, ElevatedButton, ListView, ListTile, Text, Row, Column, Container, colors, IconButton, icons

# 20231119 更新了 page 的滚动代码。
# 说明:config 文件中,以空白行来分割不同的 Host 段落,每个段落前面可以有个 #tags niit,南京这个标签
# 20231119 用 flet 这个 python 的 flutter 包来改写了一下,之前 tk 包在升级了 mac 系统后,有点说不出来的小 bug 。
# 20220630v2 左键单击改双击了
# 20220630v1 单击复制后,window 的 title 会提示复制的命令
# 程序的作用是,自动解析 .ssh/config 文件,
# tree 可以排序
# 单击会复制 ssh your_host_config 到剪贴板;
# 右键单击,会 Term 中运行 ssh your_host_config

class Win:
    def __init__(self):
        self.root = self.__win()
        self.tk_button_search_btn = self.__tk_button_search_btn()
        self.tk_input_search_content = self.__tk_input_search_content()
        self.tk_label_tip = self.__tk_label_tip()
        # self.tk_list_box_listbox = self.__tk_list_box_listbox()
        self.ttk_tree_content = self.__ttk_tree()
        results = self.getSSHConfg()
        self.updateHost2Tree(results)
        self.ttk_tree_content.bind('<Double-Button-1>', self.treeviewClick)
        self.ttk_tree_content.bind('<ButtonRelease-2>', self.treeviewDoubleClick)
        self.tk_button_search_btn.bind('<Button-1>', self.search_Host)
        self.tk_input_search_content.bind('<Key-Return>', self.search_Host)

    def __win(self):
        root = Tk()
        root.title("我是标题 ~ TkinterHelper")
        # 设置大小 居中展示
        width = 600
        height = 500
        screenwidth = root.winfo_screenwidth()
        screenheight = root.winfo_screenheight()
        geometry = '%dx%d+%d+%d' % (width, height, (screenwidth - width) / 2, (screenheight - height) / 2)
        root.geometry(geometry)
        root.resizable(width=False, height=False)
        return root

    def show(self):
        self.root.mainloop()

    def __tk_button_search_btn(self):
        btn = Button(self.root, text="检索")
        btn.place(x=290, y=20, width=50, height=24)

        return btn

    def __tk_input_search_content(self):
        ipt = Entry(self.root)
        ipt.place(x=120, y=20, width=150, height=24)
        return ipt

    def __tk_label_tip(self):
        label = Label(self.root,text="选择")
        label.place(x=40, y=20, width=50, height=24)
        return label

    def __tk_list_box_listbox(self):
        lb = Listbox(self.root)
        lb.insert(END, "列表框")
        lb.insert(END, "Python")
        lb.insert(END, "Tkinter Helper")
        lb.place(x=40, y=60, width=528, height=428)
        return lb

    def treeview_sort_column(self,tv, col, reverse):
        l = [(tv.set(k, col), k) for k in tv.get_children('')]
        l.sort(reverse=reverse)

        # rearrange items in sorted positions
        for index, (val, k) in enumerate(l):
            tv.move(k, '', index)

        # reverse sort next time
        tv.heading(col, command=lambda: \
            self.treeview_sort_column(tv, col, not reverse))
    def __ttk_tree(self):
        columns = ('Group', 'Host','tags', 'Hostname')
        tree = Treeview(self.root,columns = columns,  show='headings')
        for col in columns:
            tree.heading(col, text=col, command=lambda _col=col:self.treeview_sort_column(tree, _col, False))
        tree.grid()
        tree.place(x=40, y=60, width=528, height=428)
        return tree

    def updateHost2Tree(self,hostDatas):
        for rs in hostDatas:
            print(rs)
            third = ""
            if "Hostname" in rs:
                third = rs['Hostname']
            if "Port" in rs:
                third = third + ":" + rs['Port']
            tags=""
            if "tags" in rs:
                tags=rs['tags']
            group = 'Default'
            if "Group" in rs:
                group = rs['Group']
            li = [group, rs["Host"], tags, third+"\nabc"]
            if "color" in rs:
                self.ttk_tree_content.insert('', 'end', values=li, tags = (rs['color'],))
                colors = rs['color'].split(",",1)
                self.ttk_tree_content.tag_configure(rs['color'], background=colors[0])
            else:
                self.ttk_tree_content.insert('', 'end', values=li)

    def clearTree(self):
        x = self.ttk_tree_content.get_children()
        for item in x:
            self.ttk_tree_content.delete(item)

    def print_contents(self, event):
        print("okkkkk")

    def getSSHConfg(self):
        return getSSHConfg()

    def treeviewClick(self,event):  # 单击
        print('单击')
        for item in self.ttk_tree_content.selection():
            item_text =  self.ttk_tree_content.item(item, "values")
            print(item_text[1])  # 输出所选行的第一列的值
            pyperclip3.copy("ssh "+item_text[1])
            self.root.title("复制—— ssh " + item_text[1])

    def treeviewDoubleClick(self,event):  # 单击
        print('右键单击')
        for item in self.ttk_tree_content.selection():
            item_text =  self.ttk_tree_content.item(item, "values")
            print(item_text[1])  # 输出所选行的第一列的值
            pyperclip3.copy("ssh "+item_text[1])

    def search_Host(self ,event):
        所有结果列表 2 = self.getSSHConfg()
        要查找的字符=  self.tk_input_search_content.get()
        print("要查找的字符: ",要查找的字符)
        rs=[]
        if 要查找的字符 == "0":
            rs = 所有结果列表
        else:
            for 单个结果 in 所有结果列表 2:
                    # field 是 jieguo 这个 dict 的 key ,用 jieguo[field]获取对应的 value
                    find = False
                    for field in 单个结果:
                        if 单个结果[field].find(要查找的字符) > -1:
                            find=True
                            break
                    if find:
                        rs.append(单个结果)
        for search_rs in rs:
            print("ssh", search_rs["Host"], " \t,", search_rs)
        self.clearTree()
        self.updateHost2Tree(rs)

def getSSHConfg():
        f = open("/Users/zhangyingchun/.ssh/config")
        neirong = f.read()
        f.close();
        所有结果列表 = []
        所有段落 = neirong.split('\n\n', -1)
        i = 0;
        for 每个段落 in 所有段落:
            # 去除 .ssh/config 文件的第一段
            if i == 0:
                i = i + 1
                continue
            i = i + 1
            每个段落的所有行 = 每个段落.split("\n", -1)
            每个段落的解析结果列表 = {}
            isUsefule = False
            for 每一行 in 每个段落的所有行:
                分割后元素列表 = 每一行.split(" ", 1)
                # 处理 Host 、Hostname 、Port 、#color 、#tags 这个 5 个元素
                # print("中间调试信息 Host hang:", 每一行)
                if 每一行.startswith("Host "):
                    # 必须有 Host 的段落才会保存到 所有结果列表
                    isUsefule = True
                    每个段落的解析结果列表["Host"] = 分割后元素列表[1]
                elif 每一行.startswith("Hostname"):
                    每个段落的解析结果列表["Hostname"] = 分割后元素列表[1]
                elif 每一行.startswith("Port"):

                    每个段落的解析结果列表["Port"] = 分割后元素列表[1]
                elif 每一行.startswith("#tags"):
                    每个段落的解析结果列表["tags"] = 分割后元素列表[1]
                elif 每一行.startswith("#color"):
                    每个段落的解析结果列表["color"] = 分割后元素列表[1]
                elif 每一行.startswith("ProxyJump"):
                    每个段落的解析结果列表["ProxyJump"] = 分割后元素列表[1]
                elif 每一行.startswith("#group"):
                    每个段落的解析结果列表["Group"] = 分割后元素列表[1]
                    print("Group::: ",分割后元素列表[1])
            if isUsefule:
                所有结果列表.append(每个段落的解析结果列表)
        print("所有结果列表: ",所有结果列表)
        return 所有结果列表

class FletApp:
    def build(self, page):
        self.page = page
        self.page.title = "ssh Host 快速检索程序"
        self.search_content = TextField(on_submit=self.search_host)
        self.search_button = ElevatedButton(text="检索", on_click=self.search_host)
        self.tip_label = Text("选择")
        self.tip_label_copy=Text("复制")
        self.list_view = ListView(expand=True)
        self.copied_host = None  # 保存被复制行的标识
        # ListView 放在 Column 中,并让它扩展以填充空间
        list_container = Column(
            controls=[self.list_view],
            expand=1  # 让 Column 扩展以填充父容器
        )
        page.add(
            Column(
                controls=[
                    Row(controls=[self.tip_label, self.search_content, self.search_button, self.tip_label_copy], alignment="start"),
                    list_container  # 包含 ListView 的容器
                ],
                expand=1
            )
        )
        page.scroll = "always"

        # 更新列表
        self.update_host_to_list(self.get_ssh_config())

    def update_host_to_list(self, host_data):
        self.list_view.controls.clear()
        for i, data in enumerate(host_data):
            bg_color = ft.colors.BACKGROUND
            if i % 2 == 0 :
                bg_color= colors.GREY_100  # 交替颜色
            if data["Host"] == self.copied_host:
                bg_color = colors.BLUE_100  # 被复制行的背景色
            host=data.get("Host")
            tags=data.get("tags")

            jump = f"jump: {data.get('ProxyJump', '')}"
            if jump=="jump: ":
                jump=""
            subtitle_text = f"[{data.get('Hostname', '')}:{data.get('Port', '22')}]\t {jump}"
            title_text=f"{host}, {subtitle_text}\t{tags}"
            list_item = Container(
                content=ListTile(
                    title=Text(title_text),
                    # subtitle=Text(subtitle_text),
                    trailing=IconButton(icon=icons.CONTENT_COPY, on_click=lambda e, data=data: self.copy_ssh_command(data["Host"]))
                ),
                bgcolor=bg_color,
                padding=5
            )
            self.list_view.controls.append(list_item)
            # 添加分割线
            if i < len(host_data) - 1:
                divider = Container(height=1, width=self.page.width, bgcolor=colors.GREY_300)
                self.list_view.controls.append(divider)
        self.page.update()

    def get_ssh_config(self):
        # 这里应该是读取 SSH 配置的代码,我简化了一下
        return getSSHConfg()
        return [{"Host": "example.com", "Hostname": "192.168.1.1", "Port": "22"}, {"Host": "test.com", "Hostname": "192.168.1.2", "Port": "22"}]

    def search_host(self, e):
        search_term = self.search_content.value
        all_config = self.get_ssh_config()
        filtered_config = [
            config for config in all_config if
            search_term in config["Host"].lower() or
            search_term in config.get("Hostname", "").lower() or
            search_term in config.get("tags", "").lower()
        ]
        self.update_host_to_list(filtered_config)
        self.search_content.focus()
        # self.page.update()

    def copy_ssh_command(self, host):
        ssh_command = f"ssh {host}"
        self.copied_host = host
        self.tip_label_copy.value = ssh_command  # 更新文字
        self.tip_label_copy.update()  # 应用更改
        self.page.title = ssh_command  # 更改标题
        self.page.update()  # 应用更改
        copy_to_clipboard(ssh_command)

def copy_to_clipboard(text):
    # 这是一个示例函数,您需要根据您的操作系统实现实际的复制功能
    # 在 Windows 上,您可以使用 pyperclip 或 tkinter 的剪贴板功能
    # 在 web 应用中,您可以使用 Flet 的 clipboard 对象
    pyperclip3.copy(text)
    print("复制到剪贴板:", text)

if __name__ == "__main__":
    # 根据传入的 -tk 参数来决定启动 Tkinter 版本还是 Flet 版本的应用
    if "-tk" in sys.argv:
        # 启动 Tkinter 版本
        win = Win()
        所有结果列表 = win.getSSHConfg()
        win.show()
    else:
        # 启动 Flet 版本
        app = FletApp()
        ft.app(target=app.build)
2313 次点击
所在节点    分享创造
22 条回复
me221
2023-11-19 20:27:11 +08:00
maggch97
2023-11-19 23:23:18 +08:00
https://github.com/maggch97/sshba 之前写过一个类似的命令行的,不过配置文件是自定义了一套
atan
2023-11-19 23:26:00 +08:00
可以参考一下这个 https://hejki.org/ssheditor/
LonnyWong
2023-11-20 06:42:49 +08:00
https://github.com/trzsz/trzsz-ssh 集成到 ssh 中了,支持搜索,可分组,支持多选登录等,详见 readme 。
baobao1270
2023-11-20 08:44:42 +08:00
挺棒的,以后教小白配置 SSH 密钥登录直接把你这个软件发给他就行了(
7gugu
2023-11-20 10:50:49 +08:00
有意义的工作👍
yqf0215
363 天前
@LonnyWong 这个软件很不错。功能强大,我唯一的扩展就是在 config 每段 host 配置之前,增加了一行 #tags tip1,tip2 这行提示非常有用,在我 host 很多的时候,便于检索。仅仅一两个分组,不太够。如果 tssh 也能增加这个解析,就几乎不用我写这个软件了。
yqf0215
363 天前
@baobao1270 我这个是要自己写好 config 的,不是配置 config 的。
yqf0215
363 天前
@baobao1270 可能达不到你的要求
yqf0215
363 天前
@maggch97 很不错。只是同样只能根据 host 进行检索,我唯一的扩展就是在 config 每段 host 配置之前,增加了一行 #tags tip1,tip2 这行提示非常有用,在我 host 很多的时候,便于检索。仅仅一两个分组,不太够。
yqf0215
363 天前
@me221 c++大佬,膜拜一下
LonnyWong
362 天前
LonnyWong
361 天前
yqf0215
361 天前
@LonnyWong 太好了,赞!!!
老兄,能不能同时兼容一下我的
#tags tag1 ,tag2 这种标签啊?
LonnyWong
361 天前
@yqf0215 感觉你用 Python 读出来,然后按 #!! GroupLabels 的格式重写一下,就可以了?
LonnyWong
361 天前
trzsz ssh ( tssh ) 分组标签用法介绍: https://www.v2ex.com/t/995297
yqf0215
360 天前
@LonnyWong 虽然按 #!! GroupLabels 的格式重写一下也可以,但我写软件,水平明显不如你快啊!
再提一个功能需求,能不能增加一个 -s 参数,启动的时候,可以传入检索词,等同于进入后 输入了检索关键词。
LonnyWong
360 天前
@yqf0215 不用 -s 就会自动检索,除非你输入的刚好完全匹配了某个名字。
yqf0215
360 天前
老兄,你不肯升级兼容,那我就来升级兼容你了,
20231127 兼容 tssh 的#!! GroupLabels 备注,多个 GroupLabels 会跟 tags 合并到一起
代码在这里:
https://gist.github.com/zhangyc310/5a21dc41b2effad59bd240a19464d633
yqf0215
360 天前

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/993262

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX