1 Star 0 Fork 13

Damon / JingTerm

forked from outersky / JingTerm 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
term.py 17.41 KB
一键复制 编辑 原始数据 按行查看 历史
outersky 提交于 2023-04-12 09:48 . 默认使用solarized样式
from gi.repository import Gtk , Gdk, Vte, GLib, Pango
from context_menu import *
from keymap import *
import os
import asyncio
import time
import threading
# 终端内核组件
# 下面的是在VTE里面增加下划线提示的正则表达式
USERCHARS = "-[:alnum:]"
USERCHARS_CLASS = "[" + USERCHARS + "]"
PASSCHARS_CLASS = "[-[:alnum:]\\Q,?;.:/!%$^*&~\"#'\\E]"
HOSTCHARS_CLASS = "[-[:alnum:]]"
HOST = HOSTCHARS_CLASS + "+(\\." + HOSTCHARS_CLASS + "+)*"
PORT = "(?:\\:[[:digit:]]{1,5})?"
PATHCHARS_CLASS = "[-[:alnum:]\\Q_$.+!*,;:@&=?/~#%\\E]"
PATHTERM_CLASS = "[^\\Q]'.}>) \t\r\n,\"\\E]"
SCHEME = """(?:news:|telnet:|nntp:|file:\/|https?:|ftps?:|sftp:|webcal:|irc:|sftp:|ldaps?:|nfs:|smb:|rsync:|ssh:|rlogin:|telnet:|git:|git\+ssh:|bzr:|bzr\+ssh:|svn:|svn\+ssh:|hg:|mailto:|magnet:)"""
USERPASS = USERCHARS_CLASS + "+(?:" + PASSCHARS_CLASS + "+)?"
URLPATH = "(?:(/" + PATHCHARS_CLASS + "+(?:[(]" + PATHCHARS_CLASS + "*[)])*" + PATHCHARS_CLASS + "*)*" + PATHTERM_CLASS + ")?"
REGEX_STRINGS = [
SCHEME + "//(?:" + USERPASS + "\\@)?" + HOST + PORT + URLPATH,
"(?:www|ftp)" + HOSTCHARS_CLASS + "*\\." + HOST + PORT + URLPATH,
"(?:callto:|h323:|sip:)" + USERCHARS_CLASS + "[" + USERCHARS + ".]*(?:" + PORT + "/[a-z0-9]+)?\\@" + HOST,
"(?:mailto:)?" + USERCHARS_CLASS + "[" + USERCHARS + ".]*\\@" + HOSTCHARS_CLASS + "+\\." + HOST,
"(?:news:|man:|info:)[[:alnum:]\\Q^_{|}~!\"#$%&'()*+,./;:=?`\\E]+",
"git\\@" + HOST + ":" + HOST + URLPATH,
]
def parse(str):
color = Gdk.RGBA()
color.parse(str)
return color
class Term(Vte.Terminal):
def __init__(self,title, notebook, init_dir=None, callback=None):
super(Term, self).__init__()
#绑定参数
self.title = title
self.notebook = notebook
#设置字体,貌似GTK的css不起作用,只能在代码里面设置
font_desc = Pango.FontDescription("Noto Sans Mono 11")
self.set_font(font_desc)
# 设置前景色和背景色,和调色板
#self.set_palette_tango_dark()
self.set_palette_solarized()
#设置默认启动的shell和默认目录
home = init_dir or os.getenv('HOME')
shell = Vte.get_user_shell() or os.getenv('SHELL') or '/bin/sh'
#绑定事件:
self.connect("button_press_event",self.on_button_press)
self.connect("contents-changed", self.on_contents_changed)
self.connect("selection-changed", self.on_selection_changed)
self.connect("key-press-event", self.on_key_press_event)
self.connect("window-title-changed", self.on_window_title_changed)
self.connect("scroll-event", self.on_scroll_event)
self.connect("child-exited", self.on_child_exited)
self.eventMap = self.mapKeyEvent()
#将终端的链接等加上下划线(鼠标经过时)
self.clickable(REGEX_STRINGS)
#初始化回调函数
def _spawn_():
#VTE初始化
if not self.spawn(home, [shell]):
return
if callback:
def _f_():
callback(self)
#执行业务回调函数
GLib.timeout_add(200,_f_)
#启动
GLib.idle_add(_spawn_)
def set_palette_solarized(self):
base03 = Gdk.RGBA(0.0, 0.168, 0.211, 1.0)
base02 = Gdk.RGBA(0.0274, 0.211, 0.258, 1.0)
base01 = Gdk.RGBA(0.345, 0.431, 0.458, 1.0)
base00 = Gdk.RGBA(0.396, 0.482, 0.513, 1.0)
base0 = Gdk.RGBA(0.513, 0.580, 0.588, 1.0)
base1 = Gdk.RGBA(0.576, 0.631, 0.631, 1.0)
base2 = Gdk.RGBA(0.933, 0.909, 0.835, 1.0)
base3 = Gdk.RGBA(0.992, 0.964, 0.890, 1.0)
yellow = Gdk.RGBA(0.709, 0.537, 0.0, 1.0)
orange = Gdk.RGBA(0.796, 0.294, 0.086, 1.0)
red = Gdk.RGBA(0.862, 0.196, 0.184, 1.0)
magenta = Gdk.RGBA(0.827, 0.211, 0.509, 1.0)
violet = Gdk.RGBA(0.423, 0.443, 0.768, 1.0)
blue = Gdk.RGBA(0.149, 0.545, 0.823, 1.0)
cyan = Gdk.RGBA(0.164, 0.631, 0.596, 1.0)
green = Gdk.RGBA(0.521, 0.6, 0.0, 1.0)
palette = [
base03, # 索引0
red, # 索引1
green, # 索引2
yellow, # 索引3
blue, # 索引4
magenta, # 索引5
cyan, # 索引6
base2, # 索引7
base02, # 索引8
orange, # 索引9
base01, # 索引10
violet, # 索引11
base00, # 索引12
base0, # 索引13
base1, # 索引14
base3, # 索引15
]
self.set_colors(None, None, palette)
def set_palette_tango_dark(self):
# 定义所有颜色
colors = {
"background": parse("#2e3436"),
"foreground": parse("#eeeeec"),
"selection": parse("#729fcf"),
"selected_text": parse("#ffffff"),
"inactive_selection": parse("#ad7fa8"),
"inactive_selected_text": parse("#ffffff"),
"tooltip_background": parse("#f5f5b5"),
"tooltip_text": parse("#2e3436"),
"link": parse("#3465a4"),
"active_link": parse("#fce94f"),
"visited_link": parse("#75507b"),
}
# 创建空的颜色数组
palette = []
palette.append(colors["background"])
palette.append(colors["foreground"])
palette.append(colors["selection"])
palette.append(colors["selected_text"])
palette.append(colors["inactive_selection"])
palette.append(colors["inactive_selected_text"])
palette.append(colors["tooltip_background"])
palette.append(colors["tooltip_text"])
palette.append(colors["link"])
palette.append(colors["active_link"])
palette.append(colors["visited_link"])
# 返回颜色数组
print(palette)
self.set_colors(None,None,palette)
def rename(self,text, force=None):
self.notebook.rename(self,text,force)
def spawn(self, home, argv, callback=None):
if hasattr(self,'spawn_async'):
try:
self.spawn_async(
Vte.PtyFlags.DEFAULT,
home,
argv,
None,
0,
None,
callback,
-1,
)
return True
except BaseException as e:
print('-----Spawn1 Error',e)
return False
else:
try:
return self.spawn_sync(
Vte.PtyFlags.DEFAULT,
home,
argv,
None,
GLib.SpawnFlags.DO_NOT_REAP_CHILD,
callback,
None,
)
except BaseException as e:
print('==Spawn2 Error==',e)
return False
def on_scroll_event(self, term, scroll_event):
TERMINAL_MIN_OPACITY = 0.2
if (scroll_event.state & Gdk.ModifierType.CONTROL_MASK) != 0 and (scroll_event.state & Gdk.ModifierType.SHIFT_MASK) == 0:
try:
old_opacity = term.notebook.opacity
new_opacity = old_opacity
if scroll_event.delta_y < 0:
new_opacity = min(max(old_opacity + 0.01, TERMINAL_MIN_OPACITY), 1)
elif scroll_event.delta_y > 0:
new_opacity = min(max(old_opacity - 0.01, TERMINAL_MIN_OPACITY), 1)
if new_opacity != old_opacity:
term.notebook.update_opacity(new_opacity)
return True
except GLib.KeyFileError as e:
print("Terminal on_scroll: ", e)
return False
def mapKeyEvent(self):
return {
"Ctrl + Shift + c": lambda: self.copy_clipboard(), # 复制到剪贴板
"Ctrl + Shift + v": lambda: self.paste_clipboard(), # 从剪贴板黏贴
"Ctrl + t": lambda: self.notebook.new_term(), # 开一个新的tab
"Ctrl + w": lambda: self.notebook.close_term(self), # 关闭当前tab,如果是最后一个,则退出应用
"Ctrl + Page_Down": lambda: self.notebook.next_term(), # 切换到下一个Tab
"Ctrl + Page_Up": lambda: self.notebook.previous_term(), # 切换到前一个Tab
"Ctrl + Alt + Page_Down": lambda: self.notebook.move_to_next(), # 将Tab右移一个位置
"Ctrl + Alt + Page_Up": lambda: self.notebook.move_to_previous(), # 将Tab左移一个位置
"Ctrl + Alt + \\": lambda: self.popup_menu(), # 弹出右键菜单(包含有模板子菜单)
"Ctrl + Alt + /": lambda: self.popup_template_menu(), # 仅仅弹出模板菜单
"Ctrl + Alt + Up": lambda: self.notebook.increase_opacity(), # 增加透明度
"Ctrl + Alt + Down": lambda: self.notebook.decrease_opacity(), # 降低透明度
"Alt + f": lambda: self.notebook.window.do_max(None), # 最大化
"Alt + Left": lambda: self.resize_window(-20, 0), # 变窄
"Alt + Right": lambda: self.resize_window(20, 0), # 变宽
"Alt + Up": lambda: self.resize_window(0, -20), # 变矮
"Alt + Down": lambda: self.resize_window(0, 20), # 变高
"F2": lambda: self.notebook.rename_term() # 修改title
}
def on_key_press_event(self, widget, key_event):
keyevent_name = get_keyevent_name(key_event)
# print(keyevent_name) #打开这里可以看到每次按的是什么键
if keyevent_name in self.eventMap:
self.eventMap[keyevent_name]()
return True
def resize_window(self,delta_width, delta_height):
win = self.notebook.window
(w,h) = win.get_size()
win.resize(w+delta_width, h+delta_height)
#这个事件导致close_term调用了两次
def on_child_exited(self,widget,event):
# 如果已经在退出中了,则不再触发close_term函数
if hasattr(self,'closing') and self.closing==True:
return True
self.notebook.close_term(self) #关闭当前tab
return True
def on_contents_changed(self, widget):
(text,attr) = widget.get_text()
# print(self.title + ' content changed:\r\n', text)
def on_window_title_changed(self, widget):
self.rename(self.shorten_title(self.get_window_title()))
#简化窗口标题的路径
def shorten_title(self, title):
#将 tony@tonybook:/data/work/gitee/JingTerm
#简化成:~ok:/d/w/g/JingTerm
try:
if len(title)<20:
return title
host=""
if ':' in title:
i = title.index(':')
a = title[max(0,i-2):i]
host = a+':'
b = title[i+1:].strip()
if b.startswith('/'):
host += '/'
else:
b = title
i=b.rindex('/')
arr = [x[0] for x in b.split('/')[1:-1] ]
tail = b[i+1:]
arr.append( tail[0: min(8,len(tail)) ] )
return host + '/'.join(arr)
except Exception as e:
print("except:",e)
return title
def on_selection_changed(self, widget):
#linux下面,如果选中了,复制到主选区(可以通过鼠标中间黏贴)
if widget.get_has_selection():
widget.copy_primary()
def clipboard_has_context(self):
clipboard_text = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD).wait_for_text()
return clipboard_text != None and clipboard_text.strip() != ""
def on_button_press(self, widget, event):
if event.button==Gdk.BUTTON_PRIMARY :
# 先聚焦窗口
self.grab_focus()
# 检查是否有可点击的链接
(uri,tag) = self.match_check_event(event)
modifiers = Gtk.accelerator_get_default_mod_mask()
if (event.state & modifiers) == Gdk.ModifierType.CONTROL_MASK and uri != None:
try:
Gtk.show_uri(None, uri, Gtk.get_current_event_time())
return True
except GLib.Error as error:
try :
uri = "http://{}".format(uri)
Gtk.show_uri(None, uri, Gtk.get_current_event_time())
except GLib.Error as error:
print("Could Not Open link", error)
elif event.button==Gdk.BUTTON_SECONDARY:
self.popup_menu()
return True
#弹出主菜单
def popup_menu(self):
menu = self.create_menus()
menu.show_all()
menu.popup_at_pointer()
#弹出模板菜单
def popup_template_menu(self):
menu = self.create_template_menus()
menu.show_all()
menu.popup_at_pointer()
def create_template_menus(self):
menu = Gtk.Menu()
ContextMenu(menu, self)
menu.show_all()
menu.popup_at_pointer()
return menu
def create_menus(self):
menu = Gtk.Menu()
a = 0x40
def add_menu(label, action):
nonlocal a
a += 1
m = Gtk.MenuItem.new_with_mnemonic('_' + chr(a)+ ': ' + label)
m.action = action
m.connect("activate",self.menu_clicked)
menu.add(m)
if self.get_has_selection():
add_menu("Copy","copy")
add_menu("Reset","reset")
if self.clipboard_has_context():
add_menu("Paste","paste")
add_menu("Paste Primary","paste_primary")
add_menu("Rename","rename")
a += 1
menu_scroll = Gtk.CheckMenuItem.new_with_mnemonic('_' + chr(a)+ ': ' + 'Scroll on output')
menu_scroll.props.active = self.props.scroll_on_output
menu_scroll.action="scroll"
menu_scroll.connect("toggled", self.menu_clicked) #必须要先设置active属性,然后再绑定toggled事件,否则会多触发一次!
menu.add(menu_scroll)
a += 1
menu_template_list = Gtk.Menu()
menu_template = Gtk.MenuItem.new_with_mnemonic('_' + chr(a)+ ': ' + "Templates")
ContextMenu(menu_template_list, self)
menu_template.set_submenu(menu_template_list)
menu.add(menu_template)
return menu
def menu_clicked(self, menuitem):
action = menuitem.action
# print('clicked: ', menuitem, action)
if action=="reset":
self.reset(True,True)
self.feed_child('\r'.encode('utf-8'))
elif action=="paste":
self.paste_clipboard()
elif action=="paste_primary":
self.paste_primary ()
elif action=="rename":
self.notebook.rename_term()
elif action=="copy":
self.copy_clipboard()
elif action=="scroll":
self.props.scroll_on_output = not self.props.scroll_on_output
return True
#获取当前目录
def get_cwd(self):
pty = self.get_pty()
if pty :
pty_fd = pty.props.fd
fpid = os.tcgetpgrp(pty_fd)
if fpid > 0:
try:
# 有可能是执行了sudo 等命令,与当前用户不一样,会无法获取当前进程的cwd,需要处理 PermissionError
current_dir = os.readlink("/proc/{}/cwd".format(fpid))
return current_dir
except PermissionError as e:
return None
return None
#自动向vte输入内容
def enter(self,content):
self.feed_child(content.encode('utf-8'))
#自动向vte输入内容并回车
def enterln(self,content):
self.feed_child((content+"\r").encode('utf-8'))
#顺序执行命令
def run_commands(self,contents):
ENTERLN='enterln:'
ENTER='enter:'
SLEEP='sleep:'
TITLE='title:'
for i in range(0,len(contents)):
c = contents[i]
if c.startswith(ENTERLN):
self.enterln(c[len(ENTERLN):])
elif c.startswith(ENTER):
self.enterln(c[len(ENTER):])
elif c.startswith(TITLE):
self.rename(c[len(TITLE):],True)
elif c.startswith(SLEEP):
time.sleep(float(c[len(SLEEP):]))
#启用独立线程,执行一批命令,以免互相干扰
def start_commands_thread(self, commands):
def _f():
self.run_commands(commands)
threading.Thread(target=_f).start()
#设置vte中的可以点击的正则表达式
def clickable(self,str_arr):
for exp in str_arr:
try:
regex = Vte.Regex.for_match(exp, -1, 0x00000400) # /* PCRE2_MULTILINE */
id = self.match_add_regex(regex, 0)
self.match_set_cursor_type(id, Gdk.CursorType.HAND2)
except BaseException as e:
try:
regex = GLib.Regex(exp,
GLib.RegexCompileFlags.OPTIMIZE |
GLib.RegexCompileFlags.MULTILINE,
0)
id = self.match_add_gregex(regex, 0)
self.match_set_cursor_type(id, Gdk.CursorType.HAND2)
except BaseException as e:
print(e)
Python
1
https://gitee.com/DamonT/JingTerm.git
git@gitee.com:DamonT/JingTerm.git
DamonT
JingTerm
JingTerm
master

搜索帮助