3 Commits

Author SHA1 Message Date
0c3de1334a Fix line-based editing behavior for cut and duplicate commands
- Cut to temp buffer:
  - Respect selections by expanding to full line boundaries
  - Normalize end iterator to include full line + newline
  - Ensure consistent newline handling for last line
  - Preserve cursor position after delete
  - Prevent line merging when accumulating cut buffer

- Duplicate line:
  - Simplify logic using line-based iter APIs
  - Fix incorrect selection handling and off-by-one issues
  - Ensure full-line duplication for both selection and cursor cases
  - Correct cursor/selection restoration after duplication
2026-04-15 23:02:15 -05:00
41f3501e1f feat(code): improve comment toggling, terminal navigation, and editor event wiring
- Refactor Commenter toggle logic for line and multi-line comments
  - Preserve indentation and cursor position
  - Improve handling of existing comment detection and removal
  - Simplify bounds vs line comment dispatch

- Enhance terminal project navigation
  - Add project marker detection via Gio file traversal
  - Implement go-to-project-or-home behavior (Home key shortcut)
  - Automatically `cd` into detected project root or home directory
  - Wire terminal widget navigation through VteWidget

- Improve terminal integration
  - Pass emit_to into terminals view for event dispatching
  - Add ability for VteWidget to trigger project navigation

- Update split pane shortcut
  - Change close split view binding to Alt+\

- Add editor event support
  - Emit `text_insert` event from SourceFile on insert
  - Add new TextInsertEvent DTO and register in event system

- Misc cleanup
  - Improve imports and structure in terminals module
  - Add project marker list and filesystem traversal helpers
2026-04-15 01:54:56 -05:00
12b5fe7304 feat: improve LSP lifecycle, terminal widget, and code folding support
- LSP:
  - Add shutdown and exit request/notification handling
  - Send initialized notification after initialize response
  - Gracefully close clients with delayed shutdown via GLib timeout
  - Fix LSP WS server ↔ Godot LSP communication flow

- Terminal (VteWidget):
  - Switch to async spawn with full environment inheritance
  - Add PROMPT_COMMAND OSC7 support for cwd tracking
  - Improve UX: scrollback, no audible bell, auto scroll
  - Implement clipboard shortcuts, selection copy, middle-click paste
  - Track cwd changes and update UI label
  - Add proper signal wiring and cleanup on destroy

- Code folding:
  - Add fold support for JS, HTML, CSS, JSON, C, C++, Go
  - Reset fold state safely when AST or filetype is unavailable

- UI (Plugins dialog):
  - Improve dialog behavior (non-modal, centered, transient)
  - Add focus-out auto-hide and Ctrl+Shift+P shortcut

- Misc:
  - Add type hints in VTE widget
  - Update TODOs (remove completed items, add LSP comm fix)
  - Add terminal plugin scaffolding
2026-04-13 00:50:42 -05:00
22 changed files with 833 additions and 125 deletions

View File

@@ -1,7 +1,5 @@
___ ___
### Add ### Add
1. Add Godot LSP Client
1. Add Terminal plugin
1. Add <Ctrl\>i to **lsp_manager** to list who implements xyz 1. Add <Ctrl\>i to **lsp_manager** to list who implements xyz
___ ___
@@ -10,8 +8,8 @@ ___
___ ___
### Fix ### Fix
- Fix LSP WS Server to Godot LSP Server communication
- Fix <Ctrl\>z in multi-insert mode being funky. Insure updates happen on block level. - Fix <Ctrl\>z in multi-insert mode being funky. Insure updates happen on block level.
I.E, maybe push updates to queue to insure block undo/redo? I.E, maybe push updates to queue to insure block undo/redo?
- Fix on lsp client unload to close files lsp side and unload server endpoint
___ ___

View File

@@ -14,53 +14,94 @@ class Commenter(CodeCommentTagsMixin):
def keyboard_tggl_comment(self, buffer): def keyboard_tggl_comment(self, buffer):
language = buffer.get_language() language = buffer.get_language()
if language is None: return if not language: return
start_tag, end_tag = self.get_comment_tags(language) start_tag, end_tag = self.get_comment_tags(language)
# Note: Only handling line comment tag- no block comment option if not (start_tag or end_tag): return
if not start_tag and not end_tag: return
start_tag += " "
end_tag = end_tag or ""
bounds = buffer.get_selection_bounds() bounds = buffer.get_selection_bounds()
if bounds:
self._bounds_comment( (self._bounds_comment if bounds else self._line_comment)(
start_tag, end_tag, bounds, buffer buffer, start_tag, end_tag, bounds
) )
def _line_comment(self, buffer, start_tag: str, end_tag: str, bounds):
start = buffer.get_iter_at_mark(buffer.get_insert()).copy()
end = start.copy()
line, col = start.get_line() + 1, start.get_line_offset()
if not start.starts_line():
start.set_line_offset(0)
if not end.ends_line():
end.forward_to_line_end()
text = buffer.get_text(start, end, True)
stripped = text.lstrip()
indent = text[:-len(stripped)] if stripped else text
if stripped.startswith(start_tag):
stripped = stripped[len(start_tag):].lstrip().replace(end_tag, "", 1)
else: else:
self._line_comment(start_tag, end_tag, buffer) stripped = f"{start_tag}{stripped}{end_tag}"
def _line_comment(self, start_tag, end_tag, buffer):
start_itr = buffer.get_iter_at_mark( buffer.get_insert() ).copy()
end_itr = start_itr.copy()
if not start_itr.starts_line():
start_itr.set_line_offset(0)
if not end_itr.ends_line():
end_itr.forward_to_line_end()
text = buffer.get_text(start_itr, end_itr, True)
text = text.replace(start_tag, "") if text.startswith(start_tag) else start_tag + text
buffer.begin_user_action() buffer.begin_user_action()
buffer.delete(start_itr, end_itr) buffer.delete(start, end)
buffer.insert(start_itr, text) buffer.insert(start, indent + stripped)
buffer.end_user_action() buffer.end_user_action()
buffer.place_cursor(buffer.get_iter_at_line_offset(line, col))
def _bounds_comment(self, start_tag, end_tag, bounds, buffer): def _bounds_comment(self, buffer, start_tag: str, end_tag: str, bounds):
start_itr, end_itr = bounds def indent_len(s): return len(s) - len(s.lstrip())
if not start_itr.starts_line():
start_itr.set_line_offset(0)
if not end_itr.ends_line():
end_itr.forward_to_line_end()
text = buffer.get_text(start_itr, end_itr, True) def insert(line, idx):
text = "\n".join( return f"{line[:idx]}{start_tag}{line[idx:]}{end_tag}"
line.replace(start_tag, "") if line.startswith(start_tag) else start_tag + line
for line in text.splitlines() def process(lines):
base_indent = min(
(indent_len(l) for l in lines if l.strip()),
default = 0
) )
is_commented = all(
l.lstrip().startswith(start_tag)
for l in lines if l.strip()
)
if is_commented:
return [
l.replace(start_tag, "", 1).replace(end_tag, "", 1)
if l.lstrip().startswith(start_tag.lstrip())
else l
for l in lines
]
return [
l if not l.strip()
else insert(l, base_indent)
for l in lines
]
start, end = bounds
sline, scol = start.get_line(), start.get_line_offset()
eline, ecol = end.get_line(), end.get_line_offset()
if not start.starts_line():
start.set_line_offset(0)
if not end.ends_line():
end.forward_to_line_end()
lines = buffer.get_text(start, end, True).splitlines()
new_text = "\n".join(process(lines))
buffer.begin_user_action() buffer.begin_user_action()
buffer.delete(start_itr, end_itr) buffer.delete(start, end)
buffer.insert(start_itr, text) buffer.insert(start, new_text)
buffer.end_user_action() buffer.end_user_action()
buffer.select_range(
buffer.get_iter_at_line_offset(sline, scol),
buffer.get_iter_at_line_offset(eline, ecol),
)

View File

@@ -14,16 +14,24 @@ from .helpers import clear_temp_cut_buffer_delayed, set_temp_cut_buffer_delayed
class Handler: class Handler:
@staticmethod @staticmethod
def execute( def execute(view: GtkSource.View, *args, **kwargs):
view: GtkSource.View,
*args,
**kwargs
):
logger.debug("Command: Cut to Temp Buffer") logger.debug("Command: Cut to Temp Buffer")
clear_temp_cut_buffer_delayed(view) clear_temp_cut_buffer_delayed(view)
buffer = view.get_buffer() buffer = view.get_buffer()
if buffer.get_has_selection():
start_itr, end_itr = buffer.get_selection_bounds()
start_itr.set_line_offset(0)
if not end_itr.ends_line():
end_itr.forward_to_line_end()
if not end_itr.is_end():
end_itr.forward_char()
else:
itr = buffer.get_iter_at_mark(buffer.get_insert()) itr = buffer.get_iter_at_mark(buffer.get_insert())
start_itr = itr.copy() start_itr = itr.copy()
@@ -36,9 +44,14 @@ class Handler:
if not hasattr(view, "_cut_buffer"): if not hasattr(view, "_cut_buffer"):
view._cut_buffer = "" view._cut_buffer = ""
line_str = buffer.get_text(start_itr, end_itr, True) text = buffer.get_text(start_itr, end_itr, True)
view._cut_buffer += line_str
if not text.endswith("\n"):
text += "\n"
view._cut_buffer += text
buffer.delete(start_itr, end_itr) buffer.delete(start_itr, end_itr)
buffer.place_cursor(start_itr)
set_temp_cut_buffer_delayed(view) set_temp_cut_buffer_delayed(view)

View File

@@ -55,7 +55,7 @@ class Plugin(PluginCode):
command_name = "close_split_view", command_name = "close_split_view",
command = _close_split_view, command = _close_split_view,
binding_mode = "released", binding_mode = "released",
binding = "<Shift><Control>w" binding = "<Alt>\\"
) )
self.emit_to("source_views", event) self.emit_to("source_views", event)

View File

@@ -15,6 +15,8 @@ from ..dto.code.lsp.lsp_messages import definition_request
from ..dto.code.lsp.lsp_messages import implementation_request from ..dto.code.lsp.lsp_messages import implementation_request
from ..dto.code.lsp.lsp_messages import references_request from ..dto.code.lsp.lsp_messages import references_request
from ..dto.code.lsp.lsp_messages import symbols_request from ..dto.code.lsp.lsp_messages import symbols_request
from ..dto.code.lsp.lsp_messages import shutdown_request
from ..dto.code.lsp.lsp_messages import exit_request
@@ -36,9 +38,15 @@ class LSPClientEvents:
self._init_params["initializationOptions"] = self._init_opts self._init_params["initializationOptions"] = self._init_opts
self.send_request("initialize", self._init_params) self.send_request("initialize", self._init_params)
def send_initialized_message(self): def send_initialized_notification(self):
self.send_notification("initialized") self.send_notification("initialized")
def send_shutdown_request(self):
self.send_request("shutdown")
def send_exit_notification(self):
self.send_notification("exit")
def _lsp_did_open(self, data: dict): def _lsp_did_open(self, data: dict):
method = "textDocument/didOpen" method = "textDocument/didOpen"
params = didopen_notification["params"] params = didopen_notification["params"]

View File

@@ -183,7 +183,6 @@ references_request = {
} }
} }
symbols_request = { symbols_request = {
"method": "textDocument/documentSymbol", "method": "textDocument/documentSymbol",
"params": { "params": {
@@ -195,3 +194,14 @@ symbols_request = {
} }
} }
} }
shutdown_request = {
"method": "shutdown",
"params": None
}
exit_request = {
"method": "exit",
"params": None
}

View File

@@ -1,6 +1,9 @@
# Python imports # Python imports
# Lib imports # Lib imports
import gi
from gi.repository import GLib
# Application imports # Application imports
from libs.controllers.controller_base import ControllerBase from libs.controllers.controller_base import ControllerBase
@@ -109,9 +112,17 @@ class LSPManager(ControllerBase):
return True return True
def close_client(self, lang_id: str) -> bool: def close_client(self, lang_id: str) -> bool:
controller = self.client_manager.get_active_client()
controller.send_shutdown_request()
def _close():
self.client_manager.close_client(lang_id) self.client_manager.close_client(lang_id)
self.response_registry.close_handler(lang_id) self.response_registry.close_handler(lang_id)
return False
GLib.timeout_add(5000, _close)
return True return True
def server_response(self, lsp_response: LSPResponseTypes | dict): def server_response(self, lsp_response: LSPResponseTypes | dict):

View File

@@ -17,6 +17,10 @@ class DefaultHandler(BaseHandler):
def handle(self, method: str, response, controller): def handle(self, method: str, response, controller):
match method: match method:
case "initialize":
controller.send_initialized_notification()
case "shutdown":
controller.send_exit_notification()
case "textDocument/completion": case "textDocument/completion":
self._handle_completion(response) self._handle_completion(response)
case "textDocument/definition": case "textDocument/definition":

View File

@@ -25,6 +25,28 @@ FOLD_NODES = {
"with_statement", "with_statement",
"try_statement", "try_statement",
}, },
"javascript": {
"function_declaration",
"class_declaration",
"if_statement",
"for_statement",
"while_statement",
"switch_statement",
"try_statement",
},
"html": {
"element",
"attribute",
},
"css": {
"rule_set",
"selector",
"declaration",
},
"json": {
"object",
"array",
},
"java": { "java": {
"class_declaration", "class_declaration",
"method_declaration", "method_declaration",
@@ -35,8 +57,30 @@ FOLD_NODES = {
"switch_expression", "switch_expression",
"block", "block",
}, },
"json": { "c": {
"object", "function_definition",
"array", "struct_definition",
"if_statement",
"for_statement",
"while_statement",
"switch_statement",
},
"cpp": {
"function_definition",
"class_definition",
"struct_definition",
"namespace_definition",
"if_statement",
"for_statement",
"while_statement",
"switch_statement",
},
"go": {
"function_declaration",
"type_declaration",
"if_statement",
"for_statement",
"select_statement",
"switch_statement",
}, },
} }

View File

@@ -32,8 +32,12 @@ class Plugin(PluginCode):
file = event.response file = event.response
if not file: return if not file: return
if file.ftype not in FOLD_NODES: return if file.ftype not in FOLD_NODES:
if not hasattr(file, "ast"): return self.view.fold_start_set = {}
return
if not hasattr(file, "ast"):
self.view.fold_start_set = {}
return
buffer = file.buffer buffer = file.buffer
if not buffer.get_tag_table().lookup("invisible"): if not buffer.get_tag_table().lookup("invisible"):

View File

@@ -0,0 +1,3 @@
"""
Pligin Module
"""

View File

@@ -0,0 +1,3 @@
"""
Pligin Package
"""

View File

@@ -0,0 +1,7 @@
{
"name": "Terminals",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {}
}

View File

@@ -0,0 +1,61 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .terminals_view import TerminalsView
terminals_view = TerminalsView()
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
...
def load(self):
terminals_view.emit_to = self.emit_to
footer = self.request_ui_element("footer-container")
footer.add( terminals_view )
self._manage_signals("register_command")
def unload(self):
self._manage_signals("unregister_command")
terminals_view.destroy()
def _manage_signals(self, action: str):
event = Event_Factory.create_event(action,
command_name = "terminals",
command = Handler,
binding_mode = "released",
binding = "<Control>."
)
self.emit_to("source_views", event)
def run(self):
...
class Handler:
@staticmethod
def execute(
view: any,
*args,
**kwargs
):
logger.debug("Command: Terminal")
terminals_view.set_code_view(view)
terminals_view.hide() if terminals_view.is_visible() else terminals_view.show()

View File

@@ -0,0 +1,206 @@
# Python imports
import os
import shlex
# Lib imports
import gi
gi.require_version('Gio', '2.0')
gi.require_version('Gdk', '3.0')
gi.require_version('Gtk', '3.0')
from gi.repository import Gio
from gi.repository import Gdk
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Pango
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from .vte_widget import VteWidget
class TerminalsView(Gtk.Notebook):
def __init__(self):
super(TerminalsView, self).__init__()
self.MARKERS: list = ["src", ".git", ".gitignore", "README.md"]
self.code_view = None
self._setup_styling()
self._setup_signals()
self._load_widgets()
self.show_all()
self.hide()
def _setup_styling(self):
ctx = self.get_style_context()
ctx.add_class("terminals-view")
self.set_scrollable(True)
def _setup_signals(self):
self.connect("show", self._handle_show)
self.connect("hide", self._handle_hide)
self.connect("destroy", self._handle_destroy)
def _load_widgets(self):
hbox = Gtk.Box()
self.add_bttn = Gtk.Button(label = "")
self.hide_bttn = Gtk.Button(label = "")
self.add_bttn.connect("clicked", self._create_terminal)
self.hide_bttn.connect("clicked", self._hide_view)
hbox.add(self.add_bttn)
hbox.add(self.hide_bttn)
self.set_action_widget(hbox, Gtk.PackType.END)
self.create_terminal()
hbox.show_all()
def _generate_terminal_parts(self):
label = Gtk.Label(label = "...")
vte_widget = VteWidget()
vte_widget.hide_view = self.hide
vte_widget.go_to_project_or_home = self.go_to_project_or_home
vte_widget.create_terminal = self.create_terminal
vte_widget.close_terminal = self.close_terminal
vte_widget.prev_terminal = self.prev_terminal
vte_widget.next_terminal = self.next_terminal
label.set_text( vte_widget.get_home_path() )
label.set_tooltip_text( vte_widget.get_home_path() )
label.set_ellipsize(Pango.EllipsizeMode.START)
label.set_single_line_mode(True)
label.set_max_width_chars(32)
label.set_size_request(240, -1)
vte_widget.bind_label(label)
return label, vte_widget
def _handle_show(self, widget):
i = widget.get_current_page()
term = widget.get_nth_page(i)
GLib.idle_add(term.grab_focus)
def _handle_hide(self, widget):
if not self.code_view: return
GLib.idle_add(self.code_view.grab_focus)
def _hide_view(self, widget):
self.hide()
def _handle_destroy(self, widget):
widget.disconnect_by_func(widget._handle_show)
widget.disconnect_by_func(widget._handle_hide)
widget.disconnect_by_func(widget._handle_destroy)
self.add_bttn.disconnect_by_func(self._create_terminal)
self.hide_bttn.disconnect_by_func(self._hide_view)
def _create_terminal(self, widget):
self.create_terminal()
def has_marker(self, gfile):
try:
enumerator = gfile.enumerate_children(
"standard::name,standard::type",
Gio.FileQueryInfoFlags.NONE,
None
)
while True:
info = enumerator.next_file(None)
if info is None:
break
if info.get_name() in self.MARKERS:
enumerator.close(None)
return True
enumerator.close(None)
except Exception:
pass
return False
def find_project_path_or_home(self, current: Gio.File):
if not current: return
home = Gio.File.new_for_path( os.path.expanduser("~") )
while True:
if self.has_marker(current):
return current.get_path()
if current.equal(home):
return current.get_path()
parent = current.get_parent()
if parent is None:
return current.get_path()
current = parent
def set_code_view(self, widget):
self.code_view = widget
def go_to_project_or_home(self):
event = Event_Factory.create_event("get_file",
buffer = self.code_view.get_buffer()
)
self.emit_to("files", event)
if event.response.ftype == "buffer": return
gfile = event.response.get_location().get_parent()
fpath = self.find_project_path_or_home(gfile)
i = self.get_current_page()
widget = self.get_nth_page(i)
widget.run_command(f"cd {shlex.quote(fpath)} && clear\n")
def create_terminal(self):
label, vte_widget = self._generate_terminal_parts()
index = self.append_page(vte_widget, label)
self.set_tab_detachable(vte_widget, True)
self.set_tab_reorderable(vte_widget, True)
self.set_current_page(index)
GLib.idle_add(vte_widget.grab_focus)
self.show_all()
def close_terminal(self):
size = self.get_n_pages()
if size == 1: return
i = self.get_current_page()
widget = self.get_nth_page(i)
self.remove_page(i)
widget.destroy()
def prev_terminal(self):
i = self.get_current_page() - 1
size = self.get_n_pages()
if i < 0:
self.set_current_page(size - 1)
return
self.prev_page()
def next_terminal(self):
i = self.get_current_page() + 1
size = self.get_n_pages()
if i == size:
self.set_current_page(0)
return
self.next_page()

View File

@@ -0,0 +1,180 @@
# Python imports
import os
from os import path
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
gi.require_version('Vte', '2.91')
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GLib
from gi.repository import Vte
# Application imports
class VteWidgetException(Exception):
...
class VteWidget(Vte.Terminal):
"""
https://stackoverflow.com/questions/60454326/how-to-implement-a-linux-terminal-in-a-pygtk-app-like-vscode-and-pycharm-has
"""
def __init__(self):
super(VteWidget, self).__init__()
self._USER_HOME: str = path.expanduser('~')
self._setup_styling()
self._setup_signals()
self._subscribe_to_events()
self._load_widgets()
self._do_session_spawn()
self.show()
def _setup_styling(self):
ctx = self.get_style_context()
ctx.add_class("vte-widget")
self.set_clear_background(False)
self.set_hexpand(True)
self.set_enable_sixel(True)
self.set_cursor_shape( Vte.CursorShape.IBEAM )
self.set_audible_bell(False)
self.set_scroll_on_output(True)
def _setup_signals(self):
self.connect("commit", self._handle_commit)
self.connect("current-directory-uri-changed", self._handle_path_change)
self.connect("selection-changed", self._handle_selection)
self.connect("button-press-event", self._on_button_press)
self.connect("key-press-event", self._on_key_press)
self.connect("key-release-event", self._on_key_release)
self.connect("destroy", self._handle_destroy)
def _subscribe_to_events(self):
...
def _load_widgets(self):
...
def _do_session_spawn(self):
env_dict = os.environ.copy()
existing_pc = env_dict.get("PROMPT_COMMAND", "")
# Note: Needed for 'current-directory-uri-changed' to work.
# Make sure user .bashrc doesn't affect it...
osc7 = 'printf "\\033]7;file://%s%s\\007" "$PWD"'
env_dict.update({
"LC_ALL": "C",
"TERM": "xterm-256color",
"HISTFILE": "/dev/null",
"HISTSIZE": "0",
"HISTFILESIZE": "0",
"PS1": "\\h@\\u \\W -->: ",
"PROMPT_COMMAND": f"{osc7};{existing_pc}" if existing_pc else osc7,
})
env = [f"{k}={v}" for k, v in env_dict.items()]
self.spawn_async(
Vte.PtyFlags.DEFAULT,
self._USER_HOME,
["/bin/bash"],
env,
GLib.SpawnFlags.DEFAULT,
None, None, -1, None, None,
)
self.set_scrollback_lines(15000)
def _handle_destroy(self, terminal):
logger.debug("Destroying terminal...")
terminal.disconnect_by_func(terminal._handle_commit)
terminal.disconnect_by_func(terminal._handle_path_change)
terminal.disconnect_by_func(terminal._handle_selection)
terminal.disconnect_by_func(terminal._on_button_press)
terminal.disconnect_by_func(terminal._on_key_press)
terminal.disconnect_by_func(terminal._on_key_release)
terminal.disconnect_by_func(terminal._handle_destroy)
def _handle_commit(self, terminal, text, size):
...
def _handle_selection(self, *args):
if self.get_has_selection():
self.copy_primary()
def _on_button_press(self, widget, event):
if event.button == 2: # middle click
self.paste_clipboard()
return True
def _on_key_press(self, widget, event):
ctrl_pressed = event.state & Gdk.ModifierType.CONTROL_MASK
shift_pressed = event.state & Gdk.ModifierType.SHIFT_MASK
if event.keyval == Gdk.KEY_Home:
self.go_to_project_or_home()
return True
if ctrl_pressed:
if shift_pressed:
if event.keyval in [Gdk.KEY_C, Gdk.KEY_V]:
if event.keyval == Gdk.KEY_C:
self.copy_clipboard()
elif event.keyval == Gdk.KEY_V:
self.paste_clipboard()
return True
if event.keyval in [
Gdk.KEY_period, Gdk.KEY_t, Gdk.KEY_w, Gdk.KEY_Up, Gdk.KEY_Down
]:
if event.keyval == Gdk.KEY_period:
if hasattr(self, "hide_view"):
GLib.timeout_add(200, self.hide_view)
elif event.keyval == Gdk.KEY_t:
if hasattr(self, "create_terminal"):
self.create_terminal()
elif event.keyval == Gdk.KEY_w:
if hasattr(self, "close_terminal"):
self.close_terminal()
elif event.keyval == Gdk.KEY_Up:
if hasattr(self, "prev_terminal"):
self.prev_terminal()
elif event.keyval == Gdk.KEY_Down:
if hasattr(self, "next_terminal"):
self.next_terminal()
return True
return False
def _on_key_release(self, widget, event):
...
def _handle_path_change(self, terminal):
if not hasattr(self, "label"): return
uri = terminal.get_current_directory_uri().replace("file://", "")
terminal.label.set_text(uri)
terminal.label.set_tooltip_text(uri)
def get_home_path(self):
return self._USER_HOME
def bind_label(self, label: Gtk.Label):
self.label = label
def run_command(self, cmd: str):
self.feed_child_binary(bytes(cmd, 'utf8'))

View File

@@ -11,45 +11,45 @@ from gi.repository import GtkSource
def execute( def execute(view: GtkSource.View, *args, **kwargs):
view: GtkSource.View,
*args,
**kwargs
):
logger.debug("Command: Duplicate Line") logger.debug("Command: Duplicate Line")
buffer = view.get_buffer() buffer = view.get_buffer()
if not buffer.get_has_selection(): if buffer.get_has_selection():
had_selection = False start_itr, \
end_itr = buffer.get_selection_bounds()
start_line = start_itr.get_line()
end_line = end_itr.get_line()
scol = start_itr.get_line_offset()
ecol = end_itr.get_line_offset()
else:
itr = buffer.get_iter_at_mark(buffer.get_insert()) itr = buffer.get_iter_at_mark(buffer.get_insert())
start_itr = itr.copy() start_line = end_line = itr.get_line()
end_itr = itr.copy() col = itr.get_line_offset()
start_line = itr.get_line() + 1
start_char = itr.get_line_offset()
else:
had_selection = True
start_itr, end_itr = buffer.get_selection_bounds()
sline = start_itr.get_line()
eline = end_itr.get_line()
start_line = eline + 1
start_char = start_itr.get_line_offset()
end_char = end_itr.get_line_offset()
range_line_size = eline - sline
start_itr.backward_visible_line() start_itr = buffer.get_iter_at_line(start_line)
start_itr.forward_line() end_itr = buffer.get_iter_at_line(end_line)
end_itr.forward_line()
end_itr.backward_char()
line_str = buffer.get_slice(start_itr, end_itr, True) if not end_itr.ends_line():
end_itr.forward_to_line_end()
if not end_itr.is_end():
end_itr.forward_char() end_itr.forward_char()
buffer.insert(end_itr, f"{line_str}\n", -1)
if not had_selection: text = buffer.get_text(start_itr, end_itr, True)
new_itr = buffer.get_iter_at_line_offset(start_line, start_char) insert_itr = buffer.get_iter_at_line(end_line)
buffer.place_cursor(new_itr)
insert_itr.forward_to_line_end()
if not insert_itr.is_end():
insert_itr.forward_char()
buffer.insert(insert_itr, text)
if buffer.get_has_selection():
new_start = buffer.get_iter_at_line_offset(end_line + 1, scol)
new_end = buffer.get_iter_at_line_offset(end_line + 1 + (end_line - start_line), ecol)
buffer.select_range(new_start, new_end)
else: else:
new_itr = buffer.get_iter_at_line_offset(start_line, start_char) new_start = buffer.get_iter_at_line_offset(end_line + 1, col)
new_end_itr = buffer.get_iter_at_line_offset((start_line + range_line_size), end_char) buffer.place_cursor(new_start)
buffer.select_range(new_itr, new_end_itr)

View File

@@ -61,7 +61,20 @@ class SourceFile(GtkSource.File):
location: Gtk.TextIter, location: Gtk.TextIter,
text: str, length: int text: str, length: int
): ):
... event = Event_Factory.create_event(
"text_insert",
file = self,
buffer = self.buffer,
location = location,
text = text,
length = length
)
# Note: 'idle_add' needed b/c markers don't get thir positions
# updated relative to the initial insert.
# If not used, seg faults galor during multi insert.
# GLib.idle_add(self.emit, event)
self.emit(event)
def _after_insert_text( def _after_insert_text(
self, self,

View File

@@ -28,8 +28,8 @@ class VteWidget(Vte.Terminal):
def __init__(self): def __init__(self):
super(VteWidget, self).__init__() super(VteWidget, self).__init__()
self.cd_cmd_prefix = ("cd".encode(), "cd ".encode()) self.cd_cmd_prefix: tuple = ("cd".encode(), "cd ".encode())
self.dont_process = False self.dont_process: bool = False
self._setup_styling() self._setup_styling()
self._setup_signals() self._setup_signals()
@@ -48,9 +48,17 @@ class VteWidget(Vte.Terminal):
self.set_hexpand(True) self.set_hexpand(True)
self.set_enable_sixel(True) self.set_enable_sixel(True)
self.set_cursor_shape( Vte.CursorShape.IBEAM ) self.set_cursor_shape( Vte.CursorShape.IBEAM )
self.set_audible_bell(False)
self.set_scroll_on_output(True)
def _setup_signals(self): def _setup_signals(self):
self.connect("commit", self._commit) self.connect("commit", self._handle_commit)
self.connect("current-directory-uri-changed", self._handle_path_change)
self.connect("selection-changed", self._handle_selection)
self.connect("button-press-event", self._on_button_press)
self.connect("key-press-event", self._on_key_press)
self.connect("key-release-event", self._on_key_release)
self.connect("destroy", self._handle_destroy)
def _subscribe_to_events(self): def _subscribe_to_events(self):
event_system.subscribe("update_term_path", self.update_term_path) event_system.subscribe("update_term_path", self.update_term_path)
@@ -59,35 +67,87 @@ class VteWidget(Vte.Terminal):
... ...
def _do_session_spawn(self): def _do_session_spawn(self):
env = [ env_dict = os.environ.copy()
"DISPLAY=:0", existing_pc = env_dict.get("PROMPT_COMMAND", "")
"LC_ALL=C", # Note: Needed for 'current-directory-uri-changed' to work.
"TERM='xterm-256color'", # Make sure user .bashrc doesn't affect it...
f"HOME='{settings_manager.path_manager.get_home_path()}'", osc7 = 'printf "\\033]7;file://%s%s\\007" "$PWD"'
"XDG_RUNTIME_DIR='/run/user/1000'",
f"XAUTHORITY='{settings_manager.path_manager.get_home_path()}/.Xauthority'",
"HISTFILE=/dev/null",
"HISTSIZE=0",
"HISTFILESIZE=0",
"PS1=\\h@\\u \\W -->: ",
]
self.spawn_sync( env_dict.update({
"LC_ALL": "C",
"TERM": "xterm-256color",
"HISTFILE": "/dev/null",
"HISTSIZE": "0",
"HISTFILESIZE": "0",
"PS1": "\\h@\\u \\W -->: ",
"PROMPT_COMMAND": f"{osc7};{existing_pc}" if existing_pc else osc7,
})
env = [f"{k}={v}" for k, v in env_dict.items()]
self.spawn_async(
Vte.PtyFlags.DEFAULT, Vte.PtyFlags.DEFAULT,
settings_manager.path_manager.get_home_path(), settings_manager.path_manager.get_home_path(),
["/bin/bash"], ["/bin/bash"],
env, env,
GLib.SpawnFlags.DEFAULT, GLib.SpawnFlags.DEFAULT,
None, None, None, None, -1, None, None,
) )
startup_cmds = [ startup_cmds = [
] ]
self.set_scrollback_lines(15000)
for i in startup_cmds: for i in startup_cmds:
self.run_command(i) self.run_command(i)
def _commit(self, terminal, text, size): def _handle_destroy(self, terminal):
logger.debug("Destroying terminal...")
terminal.disconnect_by_func(terminal._handle_commit)
terminal.disconnect_by_func(terminal._handle_path_change)
terminal.disconnect_by_func(terminal._handle_selection)
terminal.disconnect_by_func(terminal._on_button_press)
terminal.disconnect_by_func(terminal._on_key_press)
terminal.disconnect_by_func(terminal._on_key_release)
terminal.disconnect_by_func(terminal._handle_destroy)
def _handle_path_change(self, terminal):
if not hasattr(self, "label"): return
uri = terminal.get_current_directory_uri().replace("file://", "")
terminal.label.set_text(uri)
terminal.label.set_tooltip_text(uri)
def _handle_selection(self, *args):
if self.get_has_selection():
self.copy_primary()
def _on_button_press(self, widget, event):
if event.button == 2: # middle click
self.paste_clipboard()
return True
def _on_key_press(self, widget, event):
ctrl_pressed = event.state & Gdk.ModifierType.CONTROL_MASK
shift_pressed = event.state & Gdk.ModifierType.SHIFT_MASK
if ctrl_pressed:
if shift_pressed:
if event.keyval in [Gdk.KEY_C, Gdk.KEY_V]:
if event.keyval == Gdk.KEY_C:
self.copy_clipboard()
elif event.keyval == Gdk.KEY_V:
self.paste_clipboard()
return True
return False
def _on_key_release(self, widget, event):
...
def _handle_commit(self, terminal, text, size):
if self.dont_process: if self.dont_process:
self.dont_process = False self.dont_process = False
return return

View File

@@ -29,6 +29,7 @@ from .cursor_moved_event import CursorMovedEvent
from .delete_range_event import DeleteRangeEvent from .delete_range_event import DeleteRangeEvent
from .modified_changed_event import ModifiedChangedEvent from .modified_changed_event import ModifiedChangedEvent
from .text_changed_event import TextChangedEvent from .text_changed_event import TextChangedEvent
from .text_insert_event import TextInsertEvent
from .text_inserted_event import TextInsertedEvent from .text_inserted_event import TextInsertedEvent
from .focused_view_event import FocusedViewEvent from .focused_view_event import FocusedViewEvent
from .set_active_file_event import SetActiveFileEvent from .set_active_file_event import SetActiveFileEvent

View File

@@ -0,0 +1,20 @@
# Python imports
from dataclasses import dataclass, field
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from .code_event import CodeEvent
@dataclass
class TextInsertEvent(CodeEvent):
location: Gtk.TextIter = None
text: str = ""
length: int = 0

View File

@@ -2,7 +2,10 @@
# Lib imports # Lib imports
import gi import gi
gi.require_version('Gdk', '3.0')
from gi.repository import Gtk from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GLib
# Application imports # Application imports
@@ -25,6 +28,7 @@ class PluginsUI(Gtk.Dialog):
self.set_title("Plugins") self.set_title("Plugins")
self.set_size_request(450, 530) self.set_size_request(450, 530)
self.set_modal(False)
self.set_deletable(False) self.set_deletable(False)
self.set_skip_pager_hint(True) self.set_skip_pager_hint(True)
self.set_skip_taskbar_hint(True) self.set_skip_taskbar_hint(True)
@@ -35,9 +39,13 @@ class PluginsUI(Gtk.Dialog):
window = widget_registery.get_object("main-window") window = widget_registery.get_object("main-window")
self.set_transient_for(window) self.set_transient_for(window)
self.set_destroy_with_parent(True)
self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
def _setup_signals(self): def _setup_signals(self):
... self.connect("focus-out-event", self._on_focus_out)
self.connect("key-release-event", self._on_key_release)
def _subscribe_to_events(self): def _subscribe_to_events(self):
... ...
@@ -59,6 +67,19 @@ class PluginsUI(Gtk.Dialog):
scrolled_win.show_all() scrolled_win.show_all()
def _on_key_release(self, widget, event):
ctrl_pressed = event.state & Gdk.ModifierType.CONTROL_MASK
shift_pressed = event.state & Gdk.ModifierType.SHIFT_MASK
if ctrl_pressed:
if shift_pressed:
if event.keyval == Gdk.KEY_P:
self.hide()
def _on_focus_out(self, *args):
self.hide()
GLib.idle_add(self.hide)
def add_row(self, manifest_meta, callback: callable): def add_row(self, manifest_meta, callback: callable):
box = Gtk.Box() box = Gtk.Box()
plugin_lbl = Gtk.Label(label = manifest_meta.manifest.name) plugin_lbl = Gtk.Label(label = manifest_meta.manifest.name)