程序的作用是,自动解析 .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:///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('', self.treeviewClick)
self.ttk_tree_content.bind('[B]', self.treeviewDoubleClick)
self.tk_button_search_btn.bind('[B]', self.search_Host)
self.tk_input_search_content.bind('', 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