Compare commits
1 Commits
master
...
356818b98c
| Author | SHA1 | Date | |
|---|---|---|---|
| 356818b98c |
31
plugins/README.txt
Normal file
31
plugins/README.txt
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
### Note
|
||||||
|
Copy the example and rename it to your desired name. Plugins define a ui target slot with the 'ui_target' requests data but don't have to if not directly interacted with.
|
||||||
|
Plugins must have a run method defined; though, you do not need to necessarily do anything within it. The run method implies that the passed in event system or other data is ready for the plugin to use.
|
||||||
|
|
||||||
|
|
||||||
|
### Manifest Example (All are required even if empty.)
|
||||||
|
```
|
||||||
|
class Manifest:
|
||||||
|
name: str = "Example Plugin"
|
||||||
|
author: str = "John Doe"
|
||||||
|
version: str = "0.0.1"
|
||||||
|
support: str = ""
|
||||||
|
pre_launch: bool = False
|
||||||
|
requests: {} = {
|
||||||
|
'pass_ui_objects': ["plugin_control_list"],
|
||||||
|
'pass_events': True,
|
||||||
|
'bind_keys': []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Requests
|
||||||
|
```
|
||||||
|
requests: {} = {
|
||||||
|
'pass_events': true, # If empty or not present will be ignored.
|
||||||
|
"pass_ui_objects": [""], # Request reference to a UI component. Will be passed back as array to plugin.
|
||||||
|
'bind_keys': [f"{name}||send_message:<Control>f"],
|
||||||
|
f"{name}||do_save:<Control>s"] # Bind keys with method and key pare using list. Must pass "name" like shown with delimiter to its right.
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Plugin Module
|
|
||||||
"""
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Plugin Package
|
|
||||||
"""
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Autopairs:
|
|
||||||
def __init__(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
def handle_word_wrap(self, buffer, char_str: str):
|
|
||||||
wrap_block = self.get_wrap_block(char_str)
|
|
||||||
if not wrap_block: return
|
|
||||||
|
|
||||||
selection = buffer.get_selection_bounds()
|
|
||||||
if not selection:
|
|
||||||
self.insert_pair(buffer, char_str, wrap_block)
|
|
||||||
return True
|
|
||||||
|
|
||||||
self.wrap_selection(buffer, char_str, wrap_block, selection)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def insert_pair(
|
|
||||||
self, buffer, char_str: str, wrap_block: tuple
|
|
||||||
):
|
|
||||||
buffer.begin_user_action()
|
|
||||||
|
|
||||||
left_block, right_block = wrap_block
|
|
||||||
insert_mark = buffer.get_insert()
|
|
||||||
|
|
||||||
insert_itr = buffer.get_iter_at_mark(insert_mark)
|
|
||||||
buffer.insert(insert_itr, f"{left_block}{right_block}")
|
|
||||||
insert_itr = buffer.get_iter_at_mark( insert_mark )
|
|
||||||
insert_itr.backward_char()
|
|
||||||
|
|
||||||
buffer.place_cursor(insert_itr)
|
|
||||||
|
|
||||||
buffer.end_user_action()
|
|
||||||
|
|
||||||
def wrap_selection(
|
|
||||||
self, buffer, char_str: str, wrap_block: tuple, selection
|
|
||||||
):
|
|
||||||
left_block, \
|
|
||||||
right_block = wrap_block
|
|
||||||
start_itr, \
|
|
||||||
end_itr = selection
|
|
||||||
data = buffer.get_text(
|
|
||||||
start_itr, end_itr, include_hidden_chars = False
|
|
||||||
)
|
|
||||||
start_mark = buffer.create_mark("startclose", start_itr, False)
|
|
||||||
end_mark = buffer.create_mark("endclose", end_itr, True)
|
|
||||||
|
|
||||||
buffer.begin_user_action()
|
|
||||||
|
|
||||||
buffer.insert(start_itr, left_block)
|
|
||||||
end_itr = buffer.get_iter_at_mark(end_mark)
|
|
||||||
buffer.insert(end_itr, right_block)
|
|
||||||
|
|
||||||
start = buffer.get_iter_at_mark(start_mark)
|
|
||||||
end = buffer.get_iter_at_mark(end_mark)
|
|
||||||
|
|
||||||
buffer.select_range(start, end)
|
|
||||||
buffer.delete_mark_by_name("startclose")
|
|
||||||
buffer.delete_mark_by_name("endclose")
|
|
||||||
|
|
||||||
buffer.end_user_action()
|
|
||||||
|
|
||||||
def get_wrap_block(self, char_str) -> tuple:
|
|
||||||
left_block = ""
|
|
||||||
right_block = ""
|
|
||||||
|
|
||||||
match char_str:
|
|
||||||
case "(" | ")":
|
|
||||||
left_block = "("
|
|
||||||
right_block = ")"
|
|
||||||
case "[" | "]":
|
|
||||||
left_block = "["
|
|
||||||
right_block = "]"
|
|
||||||
case "{" | "}":
|
|
||||||
left_block = "{"
|
|
||||||
right_block = "}"
|
|
||||||
case '"':
|
|
||||||
left_block = '"'
|
|
||||||
right_block = '"'
|
|
||||||
case "'":
|
|
||||||
left_block = "'"
|
|
||||||
right_block = "'"
|
|
||||||
case "`":
|
|
||||||
left_block = "`"
|
|
||||||
right_block = "`"
|
|
||||||
case _:
|
|
||||||
return ()
|
|
||||||
|
|
||||||
return left_block, right_block
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Autopairs",
|
|
||||||
"author": "ITDominator",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"support": "",
|
|
||||||
"requests": {}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from libs.event_factory import Event_Factory, Code_Event_Types
|
|
||||||
|
|
||||||
from plugins.plugin_types import PluginCode
|
|
||||||
|
|
||||||
from .autopairs import Autopairs
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
autopairs = Autopairs()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Plugin(PluginCode):
|
|
||||||
def __init__(self):
|
|
||||||
super(Plugin, self).__init__()
|
|
||||||
|
|
||||||
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
|
||||||
...
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
event = Event_Factory.create_event("register_command",
|
|
||||||
command_name = "autopairs",
|
|
||||||
command = Handler,
|
|
||||||
binding_mode = "held",
|
|
||||||
binding = [
|
|
||||||
"'", "`", "[", "]",
|
|
||||||
'<Shift>"',
|
|
||||||
'<Shift>(',
|
|
||||||
'<Shift>)',
|
|
||||||
'<Shift>{',
|
|
||||||
'<Shift>}'
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.emit_to("source_views", event)
|
|
||||||
|
|
||||||
def unload(self):
|
|
||||||
event = Event_Factory.create_event("unregister_command",
|
|
||||||
command_name = "autopairs",
|
|
||||||
command = Handler,
|
|
||||||
binding_mode = "held",
|
|
||||||
binding = [
|
|
||||||
"'", "`", "[", "]",
|
|
||||||
'<Shift>"',
|
|
||||||
'<Shift>(',
|
|
||||||
'<Shift>)',
|
|
||||||
'<Shift>{',
|
|
||||||
'<Shift>}'
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.emit_to("source_views", event)
|
|
||||||
autopairs = None
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class Handler:
|
|
||||||
@staticmethod
|
|
||||||
def execute(
|
|
||||||
view: any,
|
|
||||||
char_str: str,
|
|
||||||
*args,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
logger.debug("Command: Autopairs")
|
|
||||||
autopairs.handle_word_wrap(view.get_buffer(), char_str)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Plugin Module
|
|
||||||
"""
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Plugin Package
|
|
||||||
"""
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Autopairs:
|
|
||||||
def __init__(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
def handle_word_wrap(self, buffer, char_str: str):
|
|
||||||
wrap_block = self.get_wrap_block(char_str)
|
|
||||||
if not wrap_block: return
|
|
||||||
|
|
||||||
selection = buffer.get_selection_bounds()
|
|
||||||
if not selection:
|
|
||||||
self.insert_pair(buffer, char_str, wrap_block)
|
|
||||||
return True
|
|
||||||
|
|
||||||
self.wrap_selection(buffer, char_str, wrap_block, selection)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def insert_pair(
|
|
||||||
self, buffer, char_str: str, wrap_block: tuple
|
|
||||||
):
|
|
||||||
buffer.begin_user_action()
|
|
||||||
|
|
||||||
left_block, right_block = wrap_block
|
|
||||||
insert_mark = buffer.get_insert()
|
|
||||||
|
|
||||||
insert_itr = buffer.get_iter_at_mark(insert_mark)
|
|
||||||
buffer.insert(insert_itr, f"{left_block}{right_block}")
|
|
||||||
insert_itr = buffer.get_iter_at_mark( insert_mark )
|
|
||||||
insert_itr.backward_char()
|
|
||||||
|
|
||||||
buffer.place_cursor(insert_itr)
|
|
||||||
|
|
||||||
buffer.end_user_action()
|
|
||||||
|
|
||||||
def wrap_selection(
|
|
||||||
self, buffer, char_str: str, wrap_block: tuple, selection
|
|
||||||
):
|
|
||||||
left_block, \
|
|
||||||
right_block = wrap_block
|
|
||||||
start_itr, \
|
|
||||||
end_itr = selection
|
|
||||||
data = buffer.get_text(
|
|
||||||
start_itr, end_itr, include_hidden_chars = False
|
|
||||||
)
|
|
||||||
start_mark = buffer.create_mark("startclose", start_itr, False)
|
|
||||||
end_mark = buffer.create_mark("endclose", end_itr, True)
|
|
||||||
|
|
||||||
buffer.begin_user_action()
|
|
||||||
|
|
||||||
buffer.insert(start_itr, left_block)
|
|
||||||
end_itr = buffer.get_iter_at_mark(end_mark)
|
|
||||||
buffer.insert(end_itr, right_block)
|
|
||||||
|
|
||||||
start = buffer.get_iter_at_mark(start_mark)
|
|
||||||
end = buffer.get_iter_at_mark(end_mark)
|
|
||||||
|
|
||||||
buffer.select_range(start, end)
|
|
||||||
buffer.delete_mark_by_name("startclose")
|
|
||||||
buffer.delete_mark_by_name("endclose")
|
|
||||||
|
|
||||||
buffer.end_user_action()
|
|
||||||
|
|
||||||
def get_wrap_block(self, char_str) -> tuple:
|
|
||||||
left_block = ""
|
|
||||||
right_block = ""
|
|
||||||
|
|
||||||
match char_str:
|
|
||||||
case "(" | ")":
|
|
||||||
left_block = "("
|
|
||||||
right_block = ")"
|
|
||||||
case "[" | "]":
|
|
||||||
left_block = "["
|
|
||||||
right_block = "]"
|
|
||||||
case "{" | "}":
|
|
||||||
left_block = "{"
|
|
||||||
right_block = "}"
|
|
||||||
case '"':
|
|
||||||
left_block = '"'
|
|
||||||
right_block = '"'
|
|
||||||
case "'":
|
|
||||||
left_block = "'"
|
|
||||||
right_block = "'"
|
|
||||||
case "`":
|
|
||||||
left_block = "`"
|
|
||||||
right_block = "`"
|
|
||||||
case _:
|
|
||||||
return ()
|
|
||||||
|
|
||||||
return left_block, right_block
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "File History",
|
|
||||||
"author": "ITDominator",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"support": "",
|
|
||||||
"requests": {}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from libs.event_factory import Event_Factory, Code_Event_Types
|
|
||||||
|
|
||||||
from plugins.plugin_types import PluginCode
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
history: list = []
|
|
||||||
history_size: int = 30
|
|
||||||
|
|
||||||
|
|
||||||
class Plugin(PluginCode):
|
|
||||||
def __init__(self):
|
|
||||||
super(Plugin, self).__init__()
|
|
||||||
|
|
||||||
|
|
||||||
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
|
||||||
if isinstance(event, Code_Event_Types.RemovedFileEvent):
|
|
||||||
if event.file.ftype == "buffer": return
|
|
||||||
|
|
||||||
if len(history) == history_size:
|
|
||||||
history.pop(0)
|
|
||||||
|
|
||||||
history.append(event.file)
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
self._manage_signals("register_command")
|
|
||||||
|
|
||||||
def unload(self):
|
|
||||||
self._manage_signals("unregister_command")
|
|
||||||
|
|
||||||
def _manage_signals(self, action: str):
|
|
||||||
event = Event_Factory.create_event(action,
|
|
||||||
command_name = "file_history_pop",
|
|
||||||
command = Handler,
|
|
||||||
binding_mode = "released",
|
|
||||||
binding = "<Shift><Control>t"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.emit_to("source_views", event)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class Handler:
|
|
||||||
@staticmethod
|
|
||||||
def execute(
|
|
||||||
view: any,
|
|
||||||
char_str: str,
|
|
||||||
*args,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
logger.debug("Command: File History")
|
|
||||||
if len(history) == 0: return
|
|
||||||
|
|
||||||
view._on_uri_data_received(
|
|
||||||
[
|
|
||||||
history.pop().replace("file://", "")
|
|
||||||
]
|
|
||||||
)
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
|
|
||||||
gi.require_version('GtkSource', '4')
|
|
||||||
|
|
||||||
from gi.repository import GtkSource
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from .helpers import clear_temp_cut_buffer_delayed, set_temp_cut_buffer_delayed
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Handler:
|
|
||||||
@staticmethod
|
|
||||||
def execute(
|
|
||||||
view: GtkSource.View,
|
|
||||||
*args,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
logger.debug("Command: Cut to Temp Buffer")
|
|
||||||
|
|
||||||
clear_temp_cut_buffer_delayed(view)
|
|
||||||
|
|
||||||
buffer = view.get_buffer()
|
|
||||||
itr = buffer.get_iter_at_mark(buffer.get_insert())
|
|
||||||
|
|
||||||
start_itr = itr.copy()
|
|
||||||
start_itr.set_line_offset(0)
|
|
||||||
|
|
||||||
end_itr = start_itr.copy()
|
|
||||||
if not end_itr.forward_line():
|
|
||||||
end_itr = buffer.get_end_iter()
|
|
||||||
|
|
||||||
if not hasattr(view, "_cut_buffer"):
|
|
||||||
view._cut_buffer = ""
|
|
||||||
|
|
||||||
line_str = buffer.get_text(start_itr, end_itr, True)
|
|
||||||
view._cut_buffer += line_str
|
|
||||||
|
|
||||||
buffer.delete(start_itr, end_itr)
|
|
||||||
|
|
||||||
set_temp_cut_buffer_delayed(view)
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
|
|
||||||
from gi.repository import GLib
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def clear_temp_cut_buffer_delayed(view: any):
|
|
||||||
if not hasattr(view, "_cut_temp_timeout_id"): return
|
|
||||||
if not view._cut_temp_timeout_id: return
|
|
||||||
|
|
||||||
GLib.source_remove(view._cut_temp_timeout_id)
|
|
||||||
|
|
||||||
def set_temp_cut_buffer_delayed(view: any):
|
|
||||||
def clear_temp_buffer(view: any):
|
|
||||||
view._cut_buffer = ""
|
|
||||||
view._cut_temp_timeout_id = None
|
|
||||||
return False
|
|
||||||
|
|
||||||
view._cut_temp_timeout_id = GLib.timeout_add(15000, clear_temp_buffer, view)
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Nanoesq Temp Buffer",
|
|
||||||
"author": "ITDominator",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"support": "",
|
|
||||||
"requests": {}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
|
|
||||||
gi.require_version('GtkSource', '4')
|
|
||||||
|
|
||||||
from gi.repository import GLib
|
|
||||||
from gi.repository import GtkSource
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from .helpers import clear_temp_cut_buffer_delayed, set_temp_cut_buffer_delayed
|
|
||||||
|
|
||||||
|
|
||||||
class Handler2:
|
|
||||||
@staticmethod
|
|
||||||
def execute(
|
|
||||||
view: GtkSource.View,
|
|
||||||
*args,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
logger.debug("Command: Paste Temp Buffer")
|
|
||||||
if not hasattr(view, "_cut_temp_timeout_id"): return
|
|
||||||
if not hasattr(view, "_cut_buffer"): return
|
|
||||||
if not view._cut_buffer: return
|
|
||||||
|
|
||||||
clear_temp_cut_buffer_delayed(view)
|
|
||||||
|
|
||||||
buffer = view.get_buffer()
|
|
||||||
itr = buffer.get_iter_at_mark( buffer.get_insert() )
|
|
||||||
insert_itr = itr.copy()
|
|
||||||
|
|
||||||
buffer.insert(insert_itr, view._cut_buffer, -1)
|
|
||||||
|
|
||||||
set_temp_cut_buffer_delayed(view)
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from libs.event_factory import Event_Factory, Code_Event_Types
|
|
||||||
|
|
||||||
from plugins.plugin_types import PluginCode
|
|
||||||
|
|
||||||
from .cut_to_temp_buffer import Handler
|
|
||||||
from .paste_temp_buffer import Handler2
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Plugin(PluginCode):
|
|
||||||
def __init__(self):
|
|
||||||
super(Plugin, self).__init__()
|
|
||||||
|
|
||||||
|
|
||||||
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
|
||||||
...
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
self._manage_signals("register_command")
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
self._manage_signals("unregister_command")
|
|
||||||
|
|
||||||
def _manage_signals(self, action: str):
|
|
||||||
event = Event_Factory.create_event(action,
|
|
||||||
command_name = "cut_to_temp_buffer",
|
|
||||||
command = Handler,
|
|
||||||
binding_mode = "held",
|
|
||||||
binding = "<Control>k"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.emit_to("source_views", event)
|
|
||||||
|
|
||||||
event = Event_Factory.create_event(action,
|
|
||||||
command_name = "paste_temp_buffer",
|
|
||||||
command = Handler2,
|
|
||||||
binding_mode = "held",
|
|
||||||
binding = "<Control>u"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.emit_to("source_views", event)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
...
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Plugin Module
|
|
||||||
"""
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Plugin Package
|
|
||||||
"""
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Toggle Source View",
|
|
||||||
"author": "ITDominator",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"support": "",
|
|
||||||
"requests": {}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from libs.event_factory import Event_Factory, Code_Event_Types
|
|
||||||
|
|
||||||
from plugins.plugin_types import PluginCode
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Plugin(PluginCode):
|
|
||||||
def __init__(self):
|
|
||||||
super(Plugin, self).__init__()
|
|
||||||
|
|
||||||
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
|
||||||
...
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
event = Event_Factory.create_event("register_command",
|
|
||||||
command_name = "toggle_source_view",
|
|
||||||
command = Handler,
|
|
||||||
binding_mode = "released",
|
|
||||||
binding = "<Shift><Control>h"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.emit_to("source_views", event)
|
|
||||||
|
|
||||||
def unload(self):
|
|
||||||
event = Event_Factory.create_event("unregister_command",
|
|
||||||
command_name = "toggle_source_view",
|
|
||||||
command = Handler,
|
|
||||||
binding_mode = "released",
|
|
||||||
binding = "<Shift><Control>h"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.emit_to("source_views", event)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class Handler:
|
|
||||||
@staticmethod
|
|
||||||
def execute(
|
|
||||||
view: any,
|
|
||||||
char_str: str,
|
|
||||||
*args,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
logger.debug("Command: Toggle Source View")
|
|
||||||
target = view.get_parent()
|
|
||||||
target.hide() if target.is_visible() else target.show()
|
|
||||||
|
|
||||||
if view.sibling_left:
|
|
||||||
target = view.sibling_left.get_parent()
|
|
||||||
target.show()
|
|
||||||
view.sibling_left.grab_focus()
|
|
||||||
|
|
||||||
if view.sibling_right:
|
|
||||||
target = view.sibling_right.get_parent()
|
|
||||||
target.show()
|
|
||||||
view.sibling_right.grab_focus()
|
|
||||||
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Pligin Module
|
|
||||||
"""
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Pligin Package
|
|
||||||
"""
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Example Completer",
|
|
||||||
"author": "John Doe",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"support": "",
|
|
||||||
"requests": {}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
gi.require_version('Gtk', '3.0')
|
|
||||||
from gi.repository import Gtk
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from libs.event_factory import Event_Factory, Code_Event_Types
|
|
||||||
|
|
||||||
from plugins.plugin_types import PluginCode
|
|
||||||
|
|
||||||
from .provider import Provider
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Plugin(PluginCode):
|
|
||||||
def __init__(self):
|
|
||||||
super(Plugin, self).__init__()
|
|
||||||
|
|
||||||
self.provider: Provider = None
|
|
||||||
|
|
||||||
|
|
||||||
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
|
||||||
...
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
self.provider = Provider()
|
|
||||||
|
|
||||||
event = Event_Factory.create_event(
|
|
||||||
"register_provider",
|
|
||||||
provider_name = "Example Completer",
|
|
||||||
provider = self.provider,
|
|
||||||
language_ids = []
|
|
||||||
)
|
|
||||||
self.emit_to("completion", event)
|
|
||||||
|
|
||||||
def unload(self):
|
|
||||||
event = Event_Factory.create_event(
|
|
||||||
"unregister_provider",
|
|
||||||
provider_name = "Example Completer"
|
|
||||||
)
|
|
||||||
self.emit_to("completion", event)
|
|
||||||
|
|
||||||
self.provider = None
|
|
||||||
del self.provider
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
...
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
gi.require_version('GtkSource', '4')
|
|
||||||
|
|
||||||
from gi.repository import GObject
|
|
||||||
from gi.repository import GtkSource
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from .provider_response_cache import ProviderResponseCache
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Provider(GObject.GObject, GtkSource.CompletionProvider):
|
|
||||||
"""
|
|
||||||
This is a custom Completion Example Provider.
|
|
||||||
# NOTE: used information from here --> https://warroom.rsmus.com/do-that-auto-complete/
|
|
||||||
"""
|
|
||||||
__gtype_name__ = 'ExampleCompletionProvider'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(Provider, self).__init__()
|
|
||||||
|
|
||||||
self.response_cache: ProviderResponseCache = ProviderResponseCache()
|
|
||||||
|
|
||||||
|
|
||||||
def do_get_name(self):
|
|
||||||
""" Returns: a new string containing the name of the provider. """
|
|
||||||
return 'Example Code Completion'
|
|
||||||
|
|
||||||
def do_match(self, context):
|
|
||||||
# Note: If provider is in interactive activation then need to check
|
|
||||||
# view focus as otherwise non focus views start trying to grab it.
|
|
||||||
completion = context.get_property("completion")
|
|
||||||
if not completion.get_view().has_focus(): return
|
|
||||||
|
|
||||||
word = self.response_cache.get_word(context)
|
|
||||||
if not word or len(word) < 2: return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def do_get_priority(self):
|
|
||||||
""" Determin position in result list along other providor results. """
|
|
||||||
return 5
|
|
||||||
|
|
||||||
def do_activate_proposal(self, proposal, iter_):
|
|
||||||
""" Manually handle actual completion insert or set flags and handle normally. """
|
|
||||||
|
|
||||||
buffer = iter_.get_buffer()
|
|
||||||
# Note: Flag mostly intended for SourceViewsMultiInsertState
|
|
||||||
# to insure marker processes inserted text correctly.
|
|
||||||
buffer.is_processing_completion = True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def do_get_activation(self):
|
|
||||||
""" The context for when a provider will show results """
|
|
||||||
# return GtkSource.CompletionActivation.NONE
|
|
||||||
# return GtkSource.CompletionActivation.USER_REQUESTED
|
|
||||||
# return GtkSource.CompletionActivation.USER_REQUESTED | GtkSource.CompletionActivation.INTERACTIVE
|
|
||||||
return GtkSource.CompletionActivation.INTERACTIVE
|
|
||||||
|
|
||||||
def do_populate(self, context):
|
|
||||||
results = self.response_cache.filter_with_context(context)
|
|
||||||
proposals = []
|
|
||||||
|
|
||||||
for entry in results:
|
|
||||||
proposals.append(
|
|
||||||
self.response_cache.create_completion_item(
|
|
||||||
entry["label"],
|
|
||||||
entry["text"],
|
|
||||||
entry["info"]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
context.add_proposals(self, proposals, True)
|
|
||||||
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
import re
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
gi.require_version('Gtk', '3.0')
|
|
||||||
gi.require_version('GtkSource', '4')
|
|
||||||
|
|
||||||
from gi.repository import Gtk
|
|
||||||
from gi.repository import GLib
|
|
||||||
from gi.repository import GtkSource
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from libs.event_factory import Code_Event_Types
|
|
||||||
|
|
||||||
from core.widgets.code.completion_providers.provider_response_cache_base import ProviderResponseCacheBase
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ProviderResponseCache(ProviderResponseCacheBase):
|
|
||||||
def __init__(self):
|
|
||||||
super(ProviderResponseCache, self).__init__()
|
|
||||||
|
|
||||||
# Note: Using asyncio.run causes a keyboard trap that prevents app
|
|
||||||
# closure from terminal. ThreadPoolExecutor seems to not have such issues...
|
|
||||||
self.executor = ThreadPoolExecutor(max_workers = 1)
|
|
||||||
self.matchers: dict = {
|
|
||||||
"hello": {
|
|
||||||
"label": "Hello, World!",
|
|
||||||
"text": "Hello, World!",
|
|
||||||
"info": GLib.markup_escape_text( "<b>Says the first ever program developers write...</b>" )
|
|
||||||
},
|
|
||||||
"foo": {
|
|
||||||
"label": "foo",
|
|
||||||
"text": "foo }}"
|
|
||||||
},
|
|
||||||
"bar": {
|
|
||||||
"label": "bar",
|
|
||||||
"text": "bar }}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def process_file_load(self, event: Code_Event_Types.AddedNewFileEvent):
|
|
||||||
...
|
|
||||||
|
|
||||||
def process_file_close(self, event: Code_Event_Types.RemovedFileEvent):
|
|
||||||
...
|
|
||||||
|
|
||||||
def process_file_save(self, event: Code_Event_Types.SavedFileEvent):
|
|
||||||
...
|
|
||||||
|
|
||||||
def process_file_change(self, event: Code_Event_Types.TextChangedEvent):
|
|
||||||
...
|
|
||||||
|
|
||||||
def filter(self, word: str) -> list[dict]:
|
|
||||||
...
|
|
||||||
|
|
||||||
def filter_with_context(self, context) -> list[dict]:
|
|
||||||
"""
|
|
||||||
In this instance, it will do 2 things:
|
|
||||||
1) always provide Hello World! (Not ideal but an option so its in the example)
|
|
||||||
2) Utilizes the Gtk.TextIter from the TextBuffer to determine if there is a jinja
|
|
||||||
example of '{{ custom.' if so it will provide you with the options of foo and bar.
|
|
||||||
If selected it will insert foo }} or bar }}, completing your syntax...
|
|
||||||
|
|
||||||
PLEASE NOTE the GtkTextIter Logic and regex are really rough and should be adjusted and tuned
|
|
||||||
"""
|
|
||||||
|
|
||||||
proposals: list[dict] = [
|
|
||||||
{
|
|
||||||
"label": self.matchers[ "hello" ]["label"],
|
|
||||||
"text": self.matchers[ "hello" ]["text"],
|
|
||||||
"info": self.matchers[ "hello" ]["info"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Gtk Versions differ on get_iter responses...
|
|
||||||
end_iter = context.get_iter()
|
|
||||||
if not isinstance(end_iter, Gtk.TextIter):
|
|
||||||
_, end_iter = context.get_iter()
|
|
||||||
|
|
||||||
if not end_iter: return
|
|
||||||
|
|
||||||
buf = end_iter.get_buffer()
|
|
||||||
mov_iter = end_iter.copy()
|
|
||||||
if mov_iter.backward_search('{{', Gtk.TextSearchFlags.VISIBLE_ONLY):
|
|
||||||
mov_iter, _ = mov_iter.backward_search('{{', Gtk.TextSearchFlags.VISIBLE_ONLY)
|
|
||||||
left_text = buf.get_text(mov_iter, end_iter, True)
|
|
||||||
else:
|
|
||||||
left_text = ''
|
|
||||||
|
|
||||||
if re.match(r'.*\{\{\s*custom\.$', left_text):
|
|
||||||
# optionally proposed based on left search via regex
|
|
||||||
proposals.append(
|
|
||||||
{
|
|
||||||
"label": self.matchers[ "foo" ]["label"],
|
|
||||||
"text": self.matchers[ "foo" ]["text"],
|
|
||||||
"info": ""
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# optionally proposed based on left search via regex
|
|
||||||
proposals.append(
|
|
||||||
{
|
|
||||||
"label": self.matchers[ "bar" ]["label"],
|
|
||||||
"text": self.matchers[ "bar" ]["text"],
|
|
||||||
"info": ""
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return proposals
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Pligin Module
|
|
||||||
"""
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Pligin Package
|
|
||||||
"""
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .parser import load, loads
|
|
||||||
from .writer import dump, dumps
|
|
||||||
from .speg import ParseError
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
from .speg import peg
|
|
||||||
import re, sys
|
|
||||||
|
|
||||||
if sys.version_info[0] == 2:
|
|
||||||
_chr = unichr
|
|
||||||
else:
|
|
||||||
_chr = chr
|
|
||||||
|
|
||||||
def load(fin):
|
|
||||||
return loads(fin.read())
|
|
||||||
|
|
||||||
def loads(s):
|
|
||||||
if isinstance(s, bytes):
|
|
||||||
s = s.decode('utf-8')
|
|
||||||
if s.startswith(u'\ufeff'):
|
|
||||||
s = s[1:]
|
|
||||||
return peg(s.replace('\r\n', '\n'), _p_root)
|
|
||||||
|
|
||||||
def _p_ws(p):
|
|
||||||
p('[ \t]*')
|
|
||||||
|
|
||||||
def _p_nl(p):
|
|
||||||
p(r'([ \t]*(?:#[^\n]*)?\r?\n)+')
|
|
||||||
|
|
||||||
def _p_ews(p):
|
|
||||||
with p:
|
|
||||||
p(_p_nl)
|
|
||||||
p(_p_ws)
|
|
||||||
|
|
||||||
def _p_id(p):
|
|
||||||
return p(r'[$a-zA-Z_][$0-9a-zA-Z_]*')
|
|
||||||
|
|
||||||
_escape_table = {
|
|
||||||
'r': '\r',
|
|
||||||
'n': '\n',
|
|
||||||
't': '\t',
|
|
||||||
'f': '\f',
|
|
||||||
'b': '\b',
|
|
||||||
}
|
|
||||||
def _p_unescape(p):
|
|
||||||
esc = p('\\\\(?:u[0-9a-fA-F]{4}|[^\n])')
|
|
||||||
if esc[1] == 'u':
|
|
||||||
return _chr(int(esc[2:], 16))
|
|
||||||
return _escape_table.get(esc[1:], esc[1:])
|
|
||||||
|
|
||||||
_re_indent = re.compile(r'[ \t]*')
|
|
||||||
def _p_block_str(p, c):
|
|
||||||
p(r'{c}{c}{c}'.format(c=c))
|
|
||||||
lines = [['']]
|
|
||||||
with p:
|
|
||||||
while True:
|
|
||||||
s = p(r'(?:{c}(?!{c}{c})|[^{c}\\])*'.format(c=c))
|
|
||||||
l = s.split('\n')
|
|
||||||
lines[-1].append(l[0])
|
|
||||||
lines.extend([x] for x in l[1:])
|
|
||||||
if p(r'(?:\\\n[ \t]*)*'):
|
|
||||||
continue
|
|
||||||
p.commit()
|
|
||||||
lines[-1].append(p(_p_unescape))
|
|
||||||
p(r'{c}{c}{c}'.format(c=c))
|
|
||||||
|
|
||||||
lines = [''.join(l) for l in lines]
|
|
||||||
strip_ws = len(lines) > 1
|
|
||||||
if strip_ws and all(c in ' \t' for c in lines[-1]):
|
|
||||||
lines.pop()
|
|
||||||
|
|
||||||
indent = None
|
|
||||||
for line in lines[1:]:
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
if indent is None:
|
|
||||||
indent = _re_indent.match(line).group(0)
|
|
||||||
continue
|
|
||||||
for i, (c1, c2) in enumerate(zip(indent, line)):
|
|
||||||
if c1 != c2:
|
|
||||||
indent = indent[:i]
|
|
||||||
break
|
|
||||||
|
|
||||||
ind_len = len(indent or '')
|
|
||||||
if strip_ws and all(c in ' \t' for c in lines[0]):
|
|
||||||
lines = [line[ind_len:] for line in lines[1:]]
|
|
||||||
else:
|
|
||||||
lines[1:] = [line[ind_len:] for line in lines[1:]]
|
|
||||||
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
_re_mstr_nl = re.compile(r'(?:[ \t]*\n)+[ \t]*')
|
|
||||||
_re_mstr_trailing_nl = re.compile(_re_mstr_nl.pattern + r'\Z')
|
|
||||||
def _p_multiline_str(p, c):
|
|
||||||
p('{c}(?!{c}{c})(?:[ \t]*\n[ \t]*)?'.format(c=c))
|
|
||||||
string_parts = []
|
|
||||||
with p:
|
|
||||||
while True:
|
|
||||||
string_parts.append(p(r'[^{c}\\]*'.format(c=c)))
|
|
||||||
if p(r'(?:\\\n[ \t]*)*'):
|
|
||||||
string_parts.append('')
|
|
||||||
continue
|
|
||||||
p.commit()
|
|
||||||
string_parts.append(p(_p_unescape))
|
|
||||||
p(c)
|
|
||||||
string_parts[-1] = _re_mstr_trailing_nl.sub('', string_parts[-1])
|
|
||||||
string_parts[::2] = [_re_mstr_nl.sub(' ', part) for part in string_parts[::2]]
|
|
||||||
return ''.join(string_parts)
|
|
||||||
|
|
||||||
def _p_string(p):
|
|
||||||
with p:
|
|
||||||
return p(_p_block_str, '"')
|
|
||||||
with p:
|
|
||||||
return p(_p_block_str, "'")
|
|
||||||
with p:
|
|
||||||
return p(_p_multiline_str, '"')
|
|
||||||
return p(_p_multiline_str, "'")
|
|
||||||
|
|
||||||
def _p_array_value(p):
|
|
||||||
with p:
|
|
||||||
p(_p_nl)
|
|
||||||
return p(_p_object)
|
|
||||||
with p:
|
|
||||||
p(_p_ws)
|
|
||||||
return p(_p_line_object)
|
|
||||||
p(_p_ews)
|
|
||||||
return p(_p_simple_value)
|
|
||||||
|
|
||||||
def _p_key(p):
|
|
||||||
with p:
|
|
||||||
return p(_p_id)
|
|
||||||
return p(_p_string)
|
|
||||||
|
|
||||||
def _p_flow_kv(p):
|
|
||||||
k = p(_p_key)
|
|
||||||
p(_p_ews)
|
|
||||||
p(':')
|
|
||||||
with p:
|
|
||||||
p(_p_nl)
|
|
||||||
return k, p(_p_object)
|
|
||||||
with p:
|
|
||||||
p(_p_ws)
|
|
||||||
return k, p(_p_line_object)
|
|
||||||
p(_p_ews)
|
|
||||||
return k, p(_p_simple_value)
|
|
||||||
|
|
||||||
def _p_flow_obj_sep(p):
|
|
||||||
with p:
|
|
||||||
p(_p_ews)
|
|
||||||
p(',')
|
|
||||||
p(_p_ews)
|
|
||||||
return
|
|
||||||
|
|
||||||
p(_p_nl)
|
|
||||||
p(_p_ws)
|
|
||||||
|
|
||||||
def _p_simple_value(p):
|
|
||||||
with p:
|
|
||||||
p('null')
|
|
||||||
return None
|
|
||||||
|
|
||||||
with p:
|
|
||||||
p('false')
|
|
||||||
return False
|
|
||||||
with p:
|
|
||||||
p('true')
|
|
||||||
return True
|
|
||||||
|
|
||||||
with p:
|
|
||||||
return int(p('0b[01]+')[2:], 2)
|
|
||||||
with p:
|
|
||||||
return int(p('0o[0-7]+')[2:], 8)
|
|
||||||
with p:
|
|
||||||
return int(p('0x[0-9a-fA-F]+')[2:], 16)
|
|
||||||
with p:
|
|
||||||
return float(p(r'-?(?:[1-9][0-9]*|0)?\.[0-9]+(?:[Ee][\+-]?[0-9]+)?|(?:[1-9][0-9]*|0)(?:\.[0-9]+)?[Ee][\+-]?[0-9]+'))
|
|
||||||
with p:
|
|
||||||
return int(p('-?[1-9][0-9]*|0'), 10)
|
|
||||||
|
|
||||||
with p:
|
|
||||||
return p(_p_string)
|
|
||||||
|
|
||||||
with p:
|
|
||||||
p(r'\[')
|
|
||||||
r = []
|
|
||||||
with p:
|
|
||||||
p.set('I', '')
|
|
||||||
r.append(p(_p_array_value))
|
|
||||||
with p:
|
|
||||||
while True:
|
|
||||||
with p:
|
|
||||||
p(_p_ews)
|
|
||||||
p(',')
|
|
||||||
rr = p(_p_array_value)
|
|
||||||
if not p:
|
|
||||||
p(_p_nl)
|
|
||||||
with p:
|
|
||||||
rr = p(_p_object)
|
|
||||||
if not p:
|
|
||||||
p(_p_ews)
|
|
||||||
rr = p(_p_simple_value)
|
|
||||||
r.append(rr)
|
|
||||||
p.commit()
|
|
||||||
with p:
|
|
||||||
p(_p_ews)
|
|
||||||
p(',')
|
|
||||||
p(_p_ews)
|
|
||||||
p(r'\]')
|
|
||||||
return r
|
|
||||||
|
|
||||||
p(r'\{')
|
|
||||||
|
|
||||||
r = {}
|
|
||||||
p(_p_ews)
|
|
||||||
with p:
|
|
||||||
p.set('I', '')
|
|
||||||
k, v = p(_p_flow_kv)
|
|
||||||
r[k] = v
|
|
||||||
with p:
|
|
||||||
while True:
|
|
||||||
p(_p_flow_obj_sep)
|
|
||||||
k, v = p(_p_flow_kv)
|
|
||||||
r[k] = v
|
|
||||||
p.commit()
|
|
||||||
p(_p_ews)
|
|
||||||
with p:
|
|
||||||
p(',')
|
|
||||||
p(_p_ews)
|
|
||||||
p(r'\}')
|
|
||||||
return r
|
|
||||||
|
|
||||||
def _p_line_kv(p):
|
|
||||||
k = p(_p_key)
|
|
||||||
p(_p_ws)
|
|
||||||
p(':')
|
|
||||||
p(_p_ws)
|
|
||||||
with p:
|
|
||||||
p(_p_nl)
|
|
||||||
p(p.get('I'))
|
|
||||||
return k, p(_p_indented_object)
|
|
||||||
with p:
|
|
||||||
return k, p(_p_line_object)
|
|
||||||
with p:
|
|
||||||
return k, p(_p_simple_value)
|
|
||||||
p(_p_nl)
|
|
||||||
p(p.get('I'))
|
|
||||||
p('[ \t]')
|
|
||||||
p(_p_ws)
|
|
||||||
return k, p(_p_simple_value)
|
|
||||||
|
|
||||||
def _p_line_object(p):
|
|
||||||
k, v = p(_p_line_kv)
|
|
||||||
r = { k: v }
|
|
||||||
with p:
|
|
||||||
while True:
|
|
||||||
p(_p_ws)
|
|
||||||
p(',')
|
|
||||||
p(_p_ws)
|
|
||||||
k, v = p(_p_line_kv)
|
|
||||||
r[k] = v # uniqueness
|
|
||||||
p.commit()
|
|
||||||
return r
|
|
||||||
|
|
||||||
def _p_object(p):
|
|
||||||
p.set('I', p.get('I') + p('[ \t]*'))
|
|
||||||
r = p(_p_line_object)
|
|
||||||
with p:
|
|
||||||
while True:
|
|
||||||
p(_p_ws)
|
|
||||||
with p:
|
|
||||||
p(',')
|
|
||||||
p(_p_nl)
|
|
||||||
p(p.get('I'))
|
|
||||||
rr = p(_p_line_object)
|
|
||||||
r.update(rr) # unqueness
|
|
||||||
p.commit()
|
|
||||||
return r
|
|
||||||
|
|
||||||
def _p_indented_object(p):
|
|
||||||
p.set('I', p.get('I') + p('[ \t]'))
|
|
||||||
return p(_p_object)
|
|
||||||
|
|
||||||
def _p_root(p):
|
|
||||||
with p:
|
|
||||||
p(_p_nl)
|
|
||||||
|
|
||||||
with p:
|
|
||||||
p.set('I', '')
|
|
||||||
r = p(_p_object)
|
|
||||||
p(_p_ws)
|
|
||||||
with p:
|
|
||||||
p(',')
|
|
||||||
|
|
||||||
if not p:
|
|
||||||
p(_p_ws)
|
|
||||||
r = p(_p_simple_value)
|
|
||||||
|
|
||||||
p(_p_ews)
|
|
||||||
p(p.eof)
|
|
||||||
return r
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from .peg import peg, ParseError
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
import sys, re
|
|
||||||
|
|
||||||
class ParseError(Exception):
|
|
||||||
def __init__(self, msg, text, offset, line, col):
|
|
||||||
self.msg = msg
|
|
||||||
self.text = text
|
|
||||||
self.offset = offset
|
|
||||||
self.line = line
|
|
||||||
self.col = col
|
|
||||||
super(ParseError, self).__init__(msg, offset, line, col)
|
|
||||||
|
|
||||||
if sys.version_info[0] == 2:
|
|
||||||
_basestr = basestring
|
|
||||||
else:
|
|
||||||
_basestr = str
|
|
||||||
|
|
||||||
def peg(s, r):
|
|
||||||
p = _Peg(s)
|
|
||||||
try:
|
|
||||||
return p(r)
|
|
||||||
except _UnexpectedError as e:
|
|
||||||
offset = max(p._errors)
|
|
||||||
err = p._errors[offset]
|
|
||||||
raise ParseError(err.msg, s, offset, err.line, err.col)
|
|
||||||
|
|
||||||
class _UnexpectedError(RuntimeError):
|
|
||||||
def __init__(self, state, expr):
|
|
||||||
self.state = state
|
|
||||||
self.expr = expr
|
|
||||||
|
|
||||||
class _PegState:
|
|
||||||
def __init__(self, pos, line, col):
|
|
||||||
self.pos = pos
|
|
||||||
self.line = line
|
|
||||||
self.col = col
|
|
||||||
self.vars = {}
|
|
||||||
self.commited = False
|
|
||||||
|
|
||||||
class _PegError:
|
|
||||||
def __init__(self, msg, line, col):
|
|
||||||
self.msg = msg
|
|
||||||
self.line = line
|
|
||||||
self.col = col
|
|
||||||
|
|
||||||
class _Peg:
|
|
||||||
def __init__(self, s):
|
|
||||||
self._s = s
|
|
||||||
self._states = [_PegState(0, 1, 1)]
|
|
||||||
self._errors = {}
|
|
||||||
self._re_cache = {}
|
|
||||||
|
|
||||||
def __call__(self, r, *args, **kw):
|
|
||||||
if isinstance(r, _basestr):
|
|
||||||
compiled = self._re_cache.get(r)
|
|
||||||
if not compiled:
|
|
||||||
compiled = re.compile(r)
|
|
||||||
self._re_cache[r] = compiled
|
|
||||||
st = self._states[-1]
|
|
||||||
m = compiled.match(self._s[st.pos:])
|
|
||||||
if not m:
|
|
||||||
self.error(expr=r, err=kw.get('err'))
|
|
||||||
|
|
||||||
ms = m.group(0)
|
|
||||||
st.pos += len(ms)
|
|
||||||
nl_pos = ms.rfind('\n')
|
|
||||||
if nl_pos < 0:
|
|
||||||
st.col += len(ms)
|
|
||||||
else:
|
|
||||||
st.col = len(ms) - nl_pos
|
|
||||||
st.line += ms[:nl_pos].count('\n') + 1
|
|
||||||
return ms
|
|
||||||
else:
|
|
||||||
kw.pop('err', None)
|
|
||||||
return r(self, *args, **kw)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
pos = self._states[-1].pos
|
|
||||||
vars = {}
|
|
||||||
for st in self._states:
|
|
||||||
vars.update(st.vars)
|
|
||||||
return '_Peg(%r, %r)' % (self._s[:pos] + '*' + self._s[pos:], vars)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def eof(p):
|
|
||||||
if p._states[-1].pos != len(p._s):
|
|
||||||
p.error()
|
|
||||||
|
|
||||||
def error(self, err=None, expr=None):
|
|
||||||
st = self._states[-1]
|
|
||||||
if err is None:
|
|
||||||
err = 'expected {!r}, found {!r}'.format(expr, self._s[st.pos:st.pos+4])
|
|
||||||
self._errors[st.pos] = _PegError(err, st.line, st.col)
|
|
||||||
raise _UnexpectedError(st, expr)
|
|
||||||
|
|
||||||
def get(self, key, default=None):
|
|
||||||
for state in self._states[::-1]:
|
|
||||||
if key in state.vars:
|
|
||||||
return state.vars[key][0]
|
|
||||||
return default
|
|
||||||
|
|
||||||
def set(self, key, value):
|
|
||||||
self._states[-1].vars[key] = value, False
|
|
||||||
|
|
||||||
def set_global(self, key, value):
|
|
||||||
self._states[-1].vars[key] = value, True
|
|
||||||
|
|
||||||
def opt(self, *args, **kw):
|
|
||||||
with self:
|
|
||||||
return self(*args, **kw)
|
|
||||||
|
|
||||||
def not_(self, s, *args, **kw):
|
|
||||||
with self:
|
|
||||||
self(s)
|
|
||||||
self.error()
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
self._states[-1].committed = False
|
|
||||||
self._states.append(_PegState(self._states[-1].pos, self._states[-1].line, self._states[-1].col))
|
|
||||||
|
|
||||||
def __exit__(self, type, value, traceback):
|
|
||||||
if type is None:
|
|
||||||
self.commit()
|
|
||||||
self._states.pop()
|
|
||||||
return type == _UnexpectedError
|
|
||||||
|
|
||||||
def commit(self):
|
|
||||||
cur = self._states[-1]
|
|
||||||
prev = self._states[-2]
|
|
||||||
|
|
||||||
for key in cur.vars:
|
|
||||||
val, g = cur.vars[key]
|
|
||||||
if not g:
|
|
||||||
continue
|
|
||||||
if key in prev.vars:
|
|
||||||
prev.vars[key] = val, prev.vars[key][1]
|
|
||||||
else:
|
|
||||||
prev.vars[key] = val, True
|
|
||||||
|
|
||||||
prev.pos = cur.pos
|
|
||||||
prev.line = cur.line
|
|
||||||
prev.col = cur.col
|
|
||||||
prev.committed = True
|
|
||||||
|
|
||||||
def __nonzero__(self):
|
|
||||||
return self._states[-1].committed
|
|
||||||
|
|
||||||
__bool__ = __nonzero__
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
import re, json, sys
|
|
||||||
|
|
||||||
if sys.version_info[0] == 2:
|
|
||||||
def _is_num(o):
|
|
||||||
return isinstance(o, int) or isinstance(o, long) or isinstance(o, float)
|
|
||||||
def _stringify(o):
|
|
||||||
if isinstance(o, str):
|
|
||||||
return unicode(o)
|
|
||||||
if isinstance(o, unicode):
|
|
||||||
return o
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
def _is_num(o):
|
|
||||||
return isinstance(o, int) or isinstance(o, float)
|
|
||||||
def _stringify(o):
|
|
||||||
if isinstance(o, bytes):
|
|
||||||
return o.decode()
|
|
||||||
if isinstance(o, str):
|
|
||||||
return o
|
|
||||||
return None
|
|
||||||
|
|
||||||
_id_re = re.compile(r'[$a-zA-Z_][$0-9a-zA-Z_]*\Z')
|
|
||||||
|
|
||||||
class CSONEncoder:
|
|
||||||
def __init__(self, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False,
|
|
||||||
indent=None, default=None):
|
|
||||||
self._skipkeys = skipkeys
|
|
||||||
self._ensure_ascii = ensure_ascii
|
|
||||||
self._allow_nan = allow_nan
|
|
||||||
self._sort_keys = sort_keys
|
|
||||||
self._indent = ' ' * (indent or 4)
|
|
||||||
self._default = default
|
|
||||||
if check_circular:
|
|
||||||
self._obj_stack = set()
|
|
||||||
else:
|
|
||||||
self._obj_stack = None
|
|
||||||
|
|
||||||
def _format_simple_val(self, o):
|
|
||||||
if o is None:
|
|
||||||
return 'null'
|
|
||||||
if isinstance(o, bool):
|
|
||||||
return 'true' if o else 'false'
|
|
||||||
if _is_num(o):
|
|
||||||
return str(o)
|
|
||||||
s = _stringify(o)
|
|
||||||
if s is not None:
|
|
||||||
return self._escape_string(s)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _escape_string(self, s):
|
|
||||||
r = json.dumps(s, ensure_ascii=self._ensure_ascii)
|
|
||||||
return u"'{}'".format(r[1:-1].replace("'", r"\'"))
|
|
||||||
|
|
||||||
def _escape_key(self, s):
|
|
||||||
if s is None or isinstance(s, bool) or _is_num(s):
|
|
||||||
s = str(s)
|
|
||||||
s = _stringify(s)
|
|
||||||
if s is None:
|
|
||||||
if self._skipkeys:
|
|
||||||
return None
|
|
||||||
raise TypeError('keys must be a string')
|
|
||||||
if not _id_re.match(s):
|
|
||||||
return self._escape_string(s)
|
|
||||||
return s
|
|
||||||
|
|
||||||
def _push_obj(self, o):
|
|
||||||
if self._obj_stack is not None:
|
|
||||||
if id(o) in self._obj_stack:
|
|
||||||
raise ValueError('Circular reference detected')
|
|
||||||
self._obj_stack.add(id(o))
|
|
||||||
|
|
||||||
def _pop_obj(self, o):
|
|
||||||
if self._obj_stack is not None:
|
|
||||||
self._obj_stack.remove(id(o))
|
|
||||||
|
|
||||||
def _encode(self, o, obj_val=False, indent='', force_flow=False):
|
|
||||||
if isinstance(o, list):
|
|
||||||
if not o:
|
|
||||||
if obj_val:
|
|
||||||
yield ' []\n'
|
|
||||||
else:
|
|
||||||
yield indent
|
|
||||||
yield '[]\n'
|
|
||||||
else:
|
|
||||||
if obj_val:
|
|
||||||
yield ' [\n'
|
|
||||||
else:
|
|
||||||
yield indent
|
|
||||||
yield '[\n'
|
|
||||||
indent = indent + self._indent
|
|
||||||
self._push_obj(o)
|
|
||||||
for v in o:
|
|
||||||
for chunk in self._encode(v, obj_val=False, indent=indent, force_flow=True):
|
|
||||||
yield chunk
|
|
||||||
self._pop_obj(o)
|
|
||||||
yield indent[:-len(self._indent)]
|
|
||||||
yield ']\n'
|
|
||||||
elif isinstance(o, dict):
|
|
||||||
items = [(self._escape_key(k), v) for k, v in o.items()]
|
|
||||||
if self._skipkeys:
|
|
||||||
items = [(k, v) for k, v in items if k is not None]
|
|
||||||
if self._sort_keys:
|
|
||||||
items.sort()
|
|
||||||
if force_flow or not items:
|
|
||||||
if not items:
|
|
||||||
if obj_val:
|
|
||||||
yield ' {}\n'
|
|
||||||
else:
|
|
||||||
yield indent
|
|
||||||
yield '{}\n'
|
|
||||||
else:
|
|
||||||
if obj_val:
|
|
||||||
yield ' {\n'
|
|
||||||
else:
|
|
||||||
yield indent
|
|
||||||
yield '{\n'
|
|
||||||
indent = indent + self._indent
|
|
||||||
self._push_obj(o)
|
|
||||||
for k, v in items:
|
|
||||||
yield indent
|
|
||||||
yield k
|
|
||||||
yield ':'
|
|
||||||
for chunk in self._encode(v, obj_val=True, indent=indent + self._indent, force_flow=False):
|
|
||||||
yield chunk
|
|
||||||
self._pop_obj(o)
|
|
||||||
yield indent[:-len(self._indent)]
|
|
||||||
yield '}\n'
|
|
||||||
else:
|
|
||||||
if obj_val:
|
|
||||||
yield '\n'
|
|
||||||
self._push_obj(o)
|
|
||||||
for k, v in items:
|
|
||||||
yield indent
|
|
||||||
yield k
|
|
||||||
yield ':'
|
|
||||||
for chunk in self._encode(v, obj_val=True, indent=indent + self._indent, force_flow=False):
|
|
||||||
yield chunk
|
|
||||||
self._pop_obj(o)
|
|
||||||
else:
|
|
||||||
v = self._format_simple_val(o)
|
|
||||||
if v is None:
|
|
||||||
self._push_obj(o)
|
|
||||||
v = self.default(o)
|
|
||||||
for chunk in self._encode(v, obj_val=obj_val, indent=indent, force_flow=force_flow):
|
|
||||||
yield chunk
|
|
||||||
self._pop_obj(o)
|
|
||||||
else:
|
|
||||||
if obj_val:
|
|
||||||
yield ' '
|
|
||||||
else:
|
|
||||||
yield indent
|
|
||||||
yield v
|
|
||||||
yield '\n'
|
|
||||||
|
|
||||||
def iterencode(self, o):
|
|
||||||
return self._encode(o)
|
|
||||||
|
|
||||||
def encode(self, o):
|
|
||||||
return ''.join(self.iterencode(o))
|
|
||||||
|
|
||||||
def default(self, o):
|
|
||||||
if self._default is None:
|
|
||||||
raise TypeError('Cannot serialize an object of type {}'.format(type(o).__name__))
|
|
||||||
return self._default(o)
|
|
||||||
|
|
||||||
def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None,
|
|
||||||
indent=None, default=None, sort_keys=False, **kw):
|
|
||||||
if indent is None and cls is None:
|
|
||||||
return json.dump(obj, fp, skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular,
|
|
||||||
allow_nan=allow_nan, default=default, sort_keys=sort_keys, separators=(',', ':'))
|
|
||||||
|
|
||||||
if cls is None:
|
|
||||||
cls = CSONEncoder
|
|
||||||
encoder = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular,
|
|
||||||
allow_nan=allow_nan, sort_keys=sort_keys, indent=indent, default=default, **kw)
|
|
||||||
|
|
||||||
for chunk in encoder.iterencode(obj):
|
|
||||||
fp.write(chunk)
|
|
||||||
|
|
||||||
def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None,
|
|
||||||
default=None, sort_keys=False, **kw):
|
|
||||||
if indent is None and cls is None:
|
|
||||||
return json.dumps(obj, skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular,
|
|
||||||
allow_nan=allow_nan, default=default, sort_keys=sort_keys, separators=(',', ':'))
|
|
||||||
|
|
||||||
if cls is None:
|
|
||||||
cls = CSONEncoder
|
|
||||||
encoder = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular,
|
|
||||||
allow_nan=allow_nan, sort_keys=sort_keys, indent=indent, default=default, **kw)
|
|
||||||
|
|
||||||
return encoder.encode(obj)
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Snippets Completer",
|
|
||||||
"author": "ITDominator",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"support": "",
|
|
||||||
"requests": {}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
gi.require_version('Gtk', '3.0')
|
|
||||||
from gi.repository import Gtk
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from libs.event_factory import Event_Factory, Code_Event_Types
|
|
||||||
|
|
||||||
from plugins.plugin_types import PluginCode
|
|
||||||
|
|
||||||
from .provider import Provider
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Plugin(PluginCode):
|
|
||||||
def __init__(self):
|
|
||||||
super(Plugin, self).__init__()
|
|
||||||
|
|
||||||
self.provider: Provider = None
|
|
||||||
|
|
||||||
|
|
||||||
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
|
||||||
...
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
self.provider = Provider()
|
|
||||||
|
|
||||||
event = Event_Factory.create_event(
|
|
||||||
"register_provider",
|
|
||||||
provider_name = "Snippets Completer",
|
|
||||||
provider = self.provider,
|
|
||||||
language_ids = []
|
|
||||||
)
|
|
||||||
self.emit_to("completion", event)
|
|
||||||
|
|
||||||
def unload(self):
|
|
||||||
event = Event_Factory.create_event(
|
|
||||||
"unregister_provider",
|
|
||||||
provider_name = "Snippets Completer"
|
|
||||||
)
|
|
||||||
self.emit_to("completion", event)
|
|
||||||
|
|
||||||
self.provider = None
|
|
||||||
del self.provider
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
...
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
import re
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
gi.require_version('GtkSource', '4')
|
|
||||||
|
|
||||||
from gi.repository import GObject
|
|
||||||
from gi.repository import GtkSource
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from .provider_response_cache import ProviderResponseCache
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Provider(GObject.GObject, GtkSource.CompletionProvider):
|
|
||||||
"""
|
|
||||||
This is a Snippits Completion Provider.
|
|
||||||
# NOTE: used information from here --> https://warroom.rsmus.com/do-that-auto-complete/
|
|
||||||
"""
|
|
||||||
__gtype_name__ = 'SnippetsCompletionProvider'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(Provider, self).__init__()
|
|
||||||
|
|
||||||
self.response_cache: ProviderResponseCache = ProviderResponseCache()
|
|
||||||
|
|
||||||
|
|
||||||
def do_get_name(self):
|
|
||||||
return 'Snippits Completion'
|
|
||||||
|
|
||||||
def do_match(self, context):
|
|
||||||
word = self.response_cache.get_word(context)
|
|
||||||
if not word or len(word) < 2: return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def do_get_priority(self):
|
|
||||||
return 2
|
|
||||||
|
|
||||||
def do_activate_proposal(self, proposal, iter_):
|
|
||||||
buffer = iter_.get_buffer()
|
|
||||||
# Note: Flag mostly intended for SourceViewsMultiInsertState
|
|
||||||
# to insure marker processes inserted text correctly.
|
|
||||||
buffer.is_processing_completion = True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def do_get_activation(self):
|
|
||||||
""" The context for when a provider will show results """
|
|
||||||
# return GtkSource.CompletionActivation.NONE
|
|
||||||
return GtkSource.CompletionActivation.USER_REQUESTED
|
|
||||||
# return GtkSource.CompletionActivation.INTERACTIVE
|
|
||||||
|
|
||||||
def do_populate(self, context):
|
|
||||||
word = self.response_cache.get_word(context)
|
|
||||||
results = self.response_cache.filter(word)
|
|
||||||
proposals = []
|
|
||||||
|
|
||||||
for entry in results:
|
|
||||||
proposals.append(
|
|
||||||
self.response_cache.create_completion_item(
|
|
||||||
entry["label"],
|
|
||||||
entry["text"],
|
|
||||||
entry["info"]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
context.add_proposals(self, proposals, True)
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
from os import path
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
gi.require_version('GtkSource', '4')
|
|
||||||
|
|
||||||
from gi.repository import GLib
|
|
||||||
from gi.repository import GtkSource
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from libs.event_factory import Code_Event_Types
|
|
||||||
|
|
||||||
from core.widgets.code.completion_providers.provider_response_cache_base import ProviderResponseCacheBase
|
|
||||||
|
|
||||||
from . import cson
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ProviderResponseCache(ProviderResponseCacheBase):
|
|
||||||
def __init__(self):
|
|
||||||
super(ProviderResponseCache, self).__init__()
|
|
||||||
|
|
||||||
self.matchers: dict = {}
|
|
||||||
|
|
||||||
self.load_snippets()
|
|
||||||
|
|
||||||
|
|
||||||
def load_snippets(self):
|
|
||||||
fpath = path.join(path.dirname(path.realpath(__file__)), "snippets.cson")
|
|
||||||
with open(fpath, 'rb') as f:
|
|
||||||
self.snippets = cson.load(f)
|
|
||||||
for group in self.snippets:
|
|
||||||
self.snippets[group]
|
|
||||||
for entry in self.snippets[group]:
|
|
||||||
data = self.snippets[group][entry]
|
|
||||||
self.matchers[ data["prefix"] ] = {
|
|
||||||
"label": entry,
|
|
||||||
"text": data["body"],
|
|
||||||
"info": GLib.markup_escape_text( data["body"] )
|
|
||||||
}
|
|
||||||
|
|
||||||
def process_file_load(self, event: Code_Event_Types.AddedNewFileEvent):
|
|
||||||
...
|
|
||||||
|
|
||||||
def process_file_close(self, event: Code_Event_Types.RemovedFileEvent):
|
|
||||||
...
|
|
||||||
|
|
||||||
def process_file_save(self, event: Code_Event_Types.SavedFileEvent):
|
|
||||||
...
|
|
||||||
|
|
||||||
def process_file_change(self, event: Code_Event_Types.TextChangedEvent):
|
|
||||||
...
|
|
||||||
|
|
||||||
def filter(self, word: str) -> list[dict]:
|
|
||||||
response: list[dict] = []
|
|
||||||
|
|
||||||
for entry in self.matchers:
|
|
||||||
if not word in entry: continue
|
|
||||||
data = self.matchers[entry]
|
|
||||||
response.append(data)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
def filter_with_context(self, context: GtkSource.CompletionContext) -> list[dict]:
|
|
||||||
response: list[dict] = []
|
|
||||||
|
|
||||||
return response
|
|
||||||
@@ -1,614 +0,0 @@
|
|||||||
# Your snippets
|
|
||||||
#
|
|
||||||
# Atom snippets allow you to enter a simple prefix in the editor and hit tab to
|
|
||||||
# expand the prefix into a larger code block with templated values.
|
|
||||||
#
|
|
||||||
# You can create a new snippet in this file by typing "snip" and then hitting
|
|
||||||
# tab.
|
|
||||||
#
|
|
||||||
# An example CoffeeScript snippet to expand log to console.log:
|
|
||||||
#
|
|
||||||
# '.source.coffee':
|
|
||||||
# 'Console log':
|
|
||||||
# 'prefix': 'log'
|
|
||||||
# 'body': 'console.log $1'
|
|
||||||
#
|
|
||||||
# Each scope (e.g. '.source.coffee' above) can only be declared once.
|
|
||||||
#
|
|
||||||
# This file uses CoffeeScript Object Notation (CSON).
|
|
||||||
# If you are unfamiliar with CSON, you can read more about it in the
|
|
||||||
# Atom Flight Manual:
|
|
||||||
# http://flight-manual.atom.io/using-atom/sections/basic-customization/#_cson
|
|
||||||
|
|
||||||
|
|
||||||
### HTML SNIPPETS ###
|
|
||||||
'.text.html.basic':
|
|
||||||
|
|
||||||
'HTML Template':
|
|
||||||
'prefix': 'html'
|
|
||||||
'body': """<!DOCTYPE html>
|
|
||||||
<html lang="en" dir="ltr">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title></title>
|
|
||||||
<link rel="shortcut icon" href="fave_icon.png">
|
|
||||||
<link rel="stylesheet" href="resources/css/base.css">
|
|
||||||
<link rel="stylesheet" href="resources/css/main.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script src="resources/js/.js" charset="utf-8"></script>
|
|
||||||
<script src="resources/js/.js" charset="utf-8"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
'Canvas Tag':
|
|
||||||
'prefix': 'canvas'
|
|
||||||
'body': """<canvas id="canvas" width="800" height="600" style="border:1px solid #c3c3c3;"></canvas>"""
|
|
||||||
|
|
||||||
'Img Tag':
|
|
||||||
'prefix': 'img'
|
|
||||||
'body': """<img class="" src="" alt="" />"""
|
|
||||||
|
|
||||||
'Br Tag':
|
|
||||||
'prefix': 'br'
|
|
||||||
'body': """<br/>"""
|
|
||||||
|
|
||||||
'Hr Tag':
|
|
||||||
'prefix': 'hr'
|
|
||||||
'body': """<hr/>"""
|
|
||||||
|
|
||||||
'Server Side Events':
|
|
||||||
'prefix': 'sse'
|
|
||||||
'body': """// SSE events if supported
|
|
||||||
if(typeof(EventSource) !== "undefined") {
|
|
||||||
let source = new EventSource("resources/php/sse.php");
|
|
||||||
source.onmessage = (event) => {
|
|
||||||
if (event.data === "<yourDataStringToLookFor>") {
|
|
||||||
// code here
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
console.log("SSE Not Supported In Browser...");
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
'AJAX Template Function':
|
|
||||||
'prefix': 'ajax template'
|
|
||||||
'body': """const doAjax = async (actionPath, data) => {
|
|
||||||
let xhttp = new XMLHttpRequest();
|
|
||||||
|
|
||||||
xhttp.onreadystatechange = function() {
|
|
||||||
if (this.readyState === 4 && this.status === 200) {
|
|
||||||
if (this.responseText != null) { // this.responseXML if getting XML fata
|
|
||||||
handleReturnData(JSON.parse(this.responseText));
|
|
||||||
} else {
|
|
||||||
console.log("No content returned. Check the file path.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhttp.open("POST", actionPath, true);
|
|
||||||
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
|
||||||
// Force return to be JSON NOTE: Use application/xml to force XML
|
|
||||||
xhttp.overrideMimeType('application/json');
|
|
||||||
xhttp.send(data);
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
'CSS Message Colors':
|
|
||||||
'prefix': 'css colors'
|
|
||||||
'body': """.error { color: rgb(255, 0, 0); }
|
|
||||||
.warning { color: rgb(255, 168, 0); }
|
|
||||||
.success { color: rgb(136, 204, 39); }
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
### JS SNIPPETS ###
|
|
||||||
'.source.js':
|
|
||||||
|
|
||||||
'Server Side Events':
|
|
||||||
'prefix': 'sse'
|
|
||||||
'body': """// SSE events if supported
|
|
||||||
if(typeof(EventSource) !== "undefined") {
|
|
||||||
let source = new EventSource("resources/php/sse.php");
|
|
||||||
source.onmessage = (event) => {
|
|
||||||
if (event.data === "<yourDataStringToLookFor>") {
|
|
||||||
// code here
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
console.log("SSE Not Supported In Browser...");
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
'AJAX Template Function':
|
|
||||||
'prefix': 'ajax template'
|
|
||||||
'body': """const doAjax = async (actionPath, data) => {
|
|
||||||
let xhttp = new XMLHttpRequest();
|
|
||||||
|
|
||||||
xhttp.onreadystatechange = function() {
|
|
||||||
if (this.readyState === 4 && this.status === 200) {
|
|
||||||
if (this.responseText != null) { // this.responseXML if getting XML fata
|
|
||||||
handleReturnData(JSON.parse(this.responseText));
|
|
||||||
} else {
|
|
||||||
console.log("No content returned. Check the file path.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhttp.open("POST", actionPath, true);
|
|
||||||
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
|
||||||
// Force return to be JSON NOTE: Use application/xml to force XML
|
|
||||||
xhttp.overrideMimeType('application/json');
|
|
||||||
xhttp.send(data);
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
'SE6 Function':
|
|
||||||
'prefix': 'function se6'
|
|
||||||
'body': """const funcName = (arg = "") => {
|
|
||||||
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
### CSS SNIPPETS ###
|
|
||||||
'.source.css':
|
|
||||||
|
|
||||||
'CSS Message Colors':
|
|
||||||
'prefix': 'css colors'
|
|
||||||
'body': """.error { color: rgb(255, 0, 0); }
|
|
||||||
.warning { color: rgb(255, 168, 0); }
|
|
||||||
.success { color: rgb(136, 204, 39); }
|
|
||||||
"""
|
|
||||||
|
|
||||||
### PHP SNIPPETS ###
|
|
||||||
'.text.html.php':
|
|
||||||
|
|
||||||
'SSE PHP':
|
|
||||||
'prefix': 'sse php'
|
|
||||||
'body': """<?php
|
|
||||||
// Start the session
|
|
||||||
session_start();
|
|
||||||
|
|
||||||
header('Content-Type: text/event-stream');
|
|
||||||
header('Cache-Control: no-cache');
|
|
||||||
|
|
||||||
echo "data:dataToReturn\\\\n\\\\n";
|
|
||||||
|
|
||||||
flush();
|
|
||||||
?>
|
|
||||||
"""
|
|
||||||
|
|
||||||
'PHP Template':
|
|
||||||
'prefix': 'php'
|
|
||||||
'body': """<?php
|
|
||||||
// Start the session
|
|
||||||
session_start();
|
|
||||||
|
|
||||||
|
|
||||||
// Determin action
|
|
||||||
chdir("../../"); // Note: If in resources/php/
|
|
||||||
if (isset($_POST['yourPostID'])) {
|
|
||||||
// code here
|
|
||||||
} else {
|
|
||||||
$message = "Server: [Error] --> Illegal Access Method!";
|
|
||||||
serverMessage("error", $message);
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
"""
|
|
||||||
'HTML Template':
|
|
||||||
'prefix': 'html'
|
|
||||||
'body': """<!DOCTYPE html>
|
|
||||||
<html lang="en" dir="ltr">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title></title>
|
|
||||||
<link rel="shortcut icon" href="fave_icon.png">
|
|
||||||
<link rel="stylesheet" href="resources/css/base.css">
|
|
||||||
<link rel="stylesheet" href="resources/css/main.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script src="resources/js/.js" charset="utf-8"></script>
|
|
||||||
<script src="resources/js/.js" charset="utf-8"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
### BASH SNIPPETS ###
|
|
||||||
'.source.shell':
|
|
||||||
|
|
||||||
'Bash or Shell Template':
|
|
||||||
'prefix': 'bash template'
|
|
||||||
'body': """#!/bin/bash
|
|
||||||
|
|
||||||
# . CONFIG.sh
|
|
||||||
|
|
||||||
# set -o xtrace ## To debug scripts
|
|
||||||
# set -o errexit ## To exit on error
|
|
||||||
# set -o errunset ## To exit if a variable is referenced but not set
|
|
||||||
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
echo "Working Dir: " $(pwd)
|
|
||||||
|
|
||||||
file="$1"
|
|
||||||
if [ -z "${file}" ]; then
|
|
||||||
echo "ERROR: No file argument supplied..."
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -f "${file}" ]]; then
|
|
||||||
echo "SUCCESS: The path and file exists!"
|
|
||||||
else
|
|
||||||
echo "ERROR: The path or file '${file}' does NOT exist..."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
main "$@";
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
'Bash or Shell Config':
|
|
||||||
'prefix': 'bash config'
|
|
||||||
'body': """#!/bin/bash
|
|
||||||
|
|
||||||
shopt -s expand_aliases
|
|
||||||
|
|
||||||
alias echo="echo -e"
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
### PYTHON SNIPPETS ###
|
|
||||||
'.source.python':
|
|
||||||
|
|
||||||
'Glade __main__ Class Template':
|
|
||||||
'prefix': 'glade __main__ class'
|
|
||||||
'body': """#!/usr/bin/python3
|
|
||||||
|
|
||||||
|
|
||||||
# Python imports
|
|
||||||
import argparse
|
|
||||||
import faulthandler
|
|
||||||
import traceback
|
|
||||||
import signal
|
|
||||||
from setproctitle import setproctitle
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
gi.require_version('Gtk', '3.0')
|
|
||||||
from gi.repository import Gtk
|
|
||||||
from gi.repository import GLib
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from app import Application
|
|
||||||
|
|
||||||
|
|
||||||
def run():
|
|
||||||
try:
|
|
||||||
setproctitle('<replace this>')
|
|
||||||
faulthandler.enable() # For better debug info
|
|
||||||
|
|
||||||
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, Gtk.main_quit)
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
# Add long and short arguments
|
|
||||||
parser.add_argument("--debug", "-d", default="false", help="Do extra console messaging.")
|
|
||||||
parser.add_argument("--trace-debug", "-td", default="false", help="Disable saves, ignore IPC lock, do extra console messaging.")
|
|
||||||
parser.add_argument("--no-plugins", "-np", default="false", help="Do not load plugins.")
|
|
||||||
|
|
||||||
parser.add_argument("--new-tab", "-t", default="", help="Open a file into new tab.")
|
|
||||||
parser.add_argument("--new-window", "-w", default="", help="Open a file into a new window.")
|
|
||||||
|
|
||||||
# Read arguments (If any...)
|
|
||||||
args, unknownargs = parser.parse_known_args()
|
|
||||||
|
|
||||||
main = Application(args, unknownargs)
|
|
||||||
Gtk.main()
|
|
||||||
except Exception as e:
|
|
||||||
traceback.print_exc()
|
|
||||||
quit()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
''' Set process title, get arguments, and create GTK main thread. '''
|
|
||||||
run()
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
'Glade __main__ Testing Template':
|
|
||||||
'prefix': 'glade testing class'
|
|
||||||
'body': """#!/usr/bin/python3
|
|
||||||
|
|
||||||
|
|
||||||
# Python imports
|
|
||||||
import traceback
|
|
||||||
import faulthandler
|
|
||||||
import signal
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
gi.require_version('Gtk', '3.0')
|
|
||||||
from gi.repository import Gtk
|
|
||||||
from gi.repository import GLib
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
|
|
||||||
|
|
||||||
app_name = "Gtk Quick Test"
|
|
||||||
|
|
||||||
|
|
||||||
class Application(Gtk.ApplicationWindow):
|
|
||||||
def __init__(self):
|
|
||||||
super(Application, self).__init__()
|
|
||||||
self._setup_styling()
|
|
||||||
self._setup_signals()
|
|
||||||
self._load_widgets()
|
|
||||||
|
|
||||||
self.add(Gtk.Box())
|
|
||||||
|
|
||||||
self.show_all()
|
|
||||||
|
|
||||||
|
|
||||||
def _setup_styling(self):
|
|
||||||
self.set_default_size(1670, 830)
|
|
||||||
self.set_title(f"{app_name}")
|
|
||||||
# self.set_icon_from_file( settings.get_window_icon() )
|
|
||||||
self.set_gravity(5) # 5 = CENTER
|
|
||||||
self.set_position(1) # 1 = CENTER, 4 = CENTER_ALWAYS
|
|
||||||
|
|
||||||
def _setup_signals(self):
|
|
||||||
self.connect("delete-event", Gtk.main_quit)
|
|
||||||
|
|
||||||
|
|
||||||
def _load_widgets(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def run():
|
|
||||||
try:
|
|
||||||
faulthandler.enable() # For better debug info
|
|
||||||
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, Gtk.main_quit)
|
|
||||||
|
|
||||||
main = Application()
|
|
||||||
Gtk.main()
|
|
||||||
except Exception as e:
|
|
||||||
traceback.print_exc()
|
|
||||||
quit()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
''' Set process title, get arguments, and create GTK main thread. '''
|
|
||||||
run()
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
'Glade _init_ Class Template':
|
|
||||||
'prefix': 'glade __init__ class'
|
|
||||||
'body': """# Python imports
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from utils import Settings
|
|
||||||
from signal_classes import CrossClassSignals
|
|
||||||
|
|
||||||
|
|
||||||
class Main:
|
|
||||||
def __init__(self, args):
|
|
||||||
settings = Settings()
|
|
||||||
builder = settings.returnBuilder()
|
|
||||||
|
|
||||||
# Gets the methods from the classes and sets to handler.
|
|
||||||
# Then, builder connects to any signals it needs.
|
|
||||||
classes = [CrossClassSignals(settings)]
|
|
||||||
|
|
||||||
handlers = {}
|
|
||||||
for c in classes:
|
|
||||||
methods = inspect.getmembers(c, predicate=inspect.ismethod)
|
|
||||||
handlers.update(methods)
|
|
||||||
|
|
||||||
builder.connect_signals(handlers)
|
|
||||||
window = settings.createWindow()
|
|
||||||
window.show()
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
'Class Method':
|
|
||||||
'prefix': 'def1'
|
|
||||||
'body': """
|
|
||||||
def fname(self):
|
|
||||||
...
|
|
||||||
"""
|
|
||||||
|
|
||||||
'Gtk Class Method':
|
|
||||||
'prefix': 'def2'
|
|
||||||
'body': """
|
|
||||||
def fname(self, widget, eve):
|
|
||||||
...
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
'Python Glade Settings Template':
|
|
||||||
'prefix': 'glade settings class'
|
|
||||||
'body': """# Python imports
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi, cairo
|
|
||||||
gi.require_version('Gtk', '3.0')
|
|
||||||
gi.require_version('Gdk', '3.0')
|
|
||||||
|
|
||||||
from gi.repository import Gtk
|
|
||||||
from gi.repository import Gdk
|
|
||||||
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
|
|
||||||
|
|
||||||
class Settings:
|
|
||||||
def __init__(self):
|
|
||||||
self.SCRIPT_PTH = os.path.dirname(os.path.realpath(__file__)) + "/"
|
|
||||||
self.builder = Gtk.Builder()
|
|
||||||
self.builder.add_from_file(self.SCRIPT_PTH + "../resources/Main_Window.glade")
|
|
||||||
|
|
||||||
# 'Filters'
|
|
||||||
self.office = ('.doc', '.docx', '.xls', '.xlsx', '.xlt', '.xltx', '.xlm',
|
|
||||||
'.ppt', 'pptx', '.pps', '.ppsx', '.odt', '.rtf')
|
|
||||||
self.vids = ('.mkv', '.avi', '.flv', '.mov', '.m4v', '.mpg', '.wmv',
|
|
||||||
'.mpeg', '.mp4', '.webm')
|
|
||||||
self.txt = ('.txt', '.text', '.sh', '.cfg', '.conf')
|
|
||||||
self.music = ('.psf', '.mp3', '.ogg' , '.flac')
|
|
||||||
self.images = ('.png', '.jpg', '.jpeg', '.gif')
|
|
||||||
self.pdf = ('.pdf')
|
|
||||||
|
|
||||||
|
|
||||||
def createWindow(self):
|
|
||||||
# Get window and connect signals
|
|
||||||
window = self.builder.get_object("Main_Window")
|
|
||||||
window.connect("delete-event", gtk.main_quit)
|
|
||||||
self.setWindowData(window, False)
|
|
||||||
return window
|
|
||||||
|
|
||||||
def setWindowData(self, window, paintable):
|
|
||||||
screen = window.get_screen()
|
|
||||||
visual = screen.get_rgba_visual()
|
|
||||||
|
|
||||||
if visual != None and screen.is_composited():
|
|
||||||
window.set_visual(visual)
|
|
||||||
|
|
||||||
# bind css file
|
|
||||||
cssProvider = gtk.CssProvider()
|
|
||||||
cssProvider.load_from_path(self.SCRIPT_PTH + '../resources/stylesheet.css')
|
|
||||||
screen = Gdk.Screen.get_default()
|
|
||||||
styleContext = Gtk.StyleContext()
|
|
||||||
styleContext.add_provider_for_screen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_USER)
|
|
||||||
|
|
||||||
window.set_app_paintable(paintable)
|
|
||||||
if paintable:
|
|
||||||
window.connect("draw", self.area_draw)
|
|
||||||
|
|
||||||
def getMonitorData(self):
|
|
||||||
screen = self.builder.get_object("Main_Window").get_screen()
|
|
||||||
monitors = []
|
|
||||||
for m in range(screen.get_n_monitors()):
|
|
||||||
monitors.append(screen.get_monitor_geometry(m))
|
|
||||||
|
|
||||||
for monitor in monitors:
|
|
||||||
print(str(monitor.width) + "x" + str(monitor.height) + "+" + str(monitor.x) + "+" + str(monitor.y))
|
|
||||||
|
|
||||||
return monitors
|
|
||||||
|
|
||||||
def area_draw(self, widget, cr):
|
|
||||||
cr.set_source_rgba(0, 0, 0, 0.54)
|
|
||||||
cr.set_operator(cairo.OPERATOR_SOURCE)
|
|
||||||
cr.paint()
|
|
||||||
cr.set_operator(cairo.OPERATOR_OVER)
|
|
||||||
|
|
||||||
|
|
||||||
def returnBuilder(self): return self.builder
|
|
||||||
|
|
||||||
# Filter returns
|
|
||||||
def returnOfficeFilter(self): return self.office
|
|
||||||
def returnVidsFilter(self): return self.vids
|
|
||||||
def returnTextFilter(self): return self.txt
|
|
||||||
def returnMusicFilter(self): return self.music
|
|
||||||
def returnImagesFilter(self): return self.images
|
|
||||||
def returnPdfFilter(self): return self.pdf
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
'Python Glade CrossClassSignals Template':
|
|
||||||
'prefix': 'glade crossClassSignals class'
|
|
||||||
'body': """# Python imports
|
|
||||||
import threading
|
|
||||||
import subprocess
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
|
|
||||||
|
|
||||||
def threaded(fn):
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
threading.Thread(target=fn, args=args, kwargs=kwargs).start()
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
class CrossClassSignals:
|
|
||||||
def __init__(self, settings):
|
|
||||||
self.settings = settings
|
|
||||||
self.builder = self.settings.returnBuilder()
|
|
||||||
|
|
||||||
|
|
||||||
def getClipboardData(self):
|
|
||||||
proc = subprocess.Popen(['xclip','-selection', 'clipboard', '-o'], stdout=subprocess.PIPE)
|
|
||||||
retcode = proc.wait()
|
|
||||||
data = proc.stdout.read()
|
|
||||||
return data.decode("utf-8").strip()
|
|
||||||
|
|
||||||
def setClipboardData(self, data):
|
|
||||||
proc = subprocess.Popen(['xclip','-selection','clipboard'], stdin=subprocess.PIPE)
|
|
||||||
proc.stdin.write(data)
|
|
||||||
proc.stdin.close()
|
|
||||||
retcode = proc.wait()
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
'Python Glade Generic Template':
|
|
||||||
'prefix': 'glade generic class'
|
|
||||||
'body': """# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
|
|
||||||
|
|
||||||
class GenericClass:
|
|
||||||
def __init__(self):
|
|
||||||
super(GenericClass, self).__init__()
|
|
||||||
|
|
||||||
self._setup_styling()
|
|
||||||
self._setup_signals()
|
|
||||||
self._subscribe_to_events()
|
|
||||||
self._load_widgets()
|
|
||||||
|
|
||||||
|
|
||||||
def _setup_styling(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
def _setup_signals(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
def _subscribe_to_events(self):
|
|
||||||
event_system.subscribe("handle_file_from_ipc", self.handle_file_from_ipc)
|
|
||||||
|
|
||||||
def _load_widgets(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
"""
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Pligin Module
|
|
||||||
"""
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Pligin Package
|
|
||||||
"""
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
import re
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
gi.require_version('Gtk', '3.0')
|
|
||||||
gi.require_version('GtkSource', '4')
|
|
||||||
|
|
||||||
from gi.repository import GObject
|
|
||||||
from gi.repository import Gtk
|
|
||||||
from gi.repository import GtkSource
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from .provider_response_cache import ProviderResponseCache
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Provider(GtkSource.CompletionWords):
|
|
||||||
"""
|
|
||||||
This is a Words Completion Provider.
|
|
||||||
# NOTE: used information from here --> https://warroom.rsmus.com/do-that-auto-complete/
|
|
||||||
"""
|
|
||||||
__gtype_name__ = 'WordsCompletionProvider'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(Provider, self).__init__()
|
|
||||||
|
|
||||||
self.response_cache: ProviderResponseCache = ProviderResponseCache()
|
|
||||||
|
|
||||||
|
|
||||||
def do_get_name(self):
|
|
||||||
return 'Words Completion'
|
|
||||||
|
|
||||||
def do_match(self, context):
|
|
||||||
# Note: If provider is in interactive activation then need to check
|
|
||||||
# view focus as otherwise non focus views start trying to grab it.
|
|
||||||
completion = context.get_property("completion")
|
|
||||||
if not completion.get_view().has_focus(): return
|
|
||||||
|
|
||||||
word = self.response_cache.get_word(context)
|
|
||||||
if not word or len(word) < 2: return False
|
|
||||||
|
|
||||||
iter = self.response_cache.get_iter_correctly(context)
|
|
||||||
iter.backward_char()
|
|
||||||
ch = iter.get_char()
|
|
||||||
# NOTE: Look to re-add or apply supprting logic to use spaces
|
|
||||||
# As is it slows down the editor in certain contexts...
|
|
||||||
# if not (ch in ('_', '.', ' ') or ch.isalnum()):
|
|
||||||
if not (ch in ('_', '.') or ch.isalnum()):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def do_get_priority(self):
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def do_activate_proposal(self, proposal, iter_):
|
|
||||||
buffer = iter_.get_buffer()
|
|
||||||
# Note: Flag mostly intended for SourceViewsMultiInsertState
|
|
||||||
# to insure marker processes inserted text correctly.
|
|
||||||
buffer.is_processing_completion = True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def do_get_activation(self):
|
|
||||||
""" The context for when a provider will show results """
|
|
||||||
# return GtkSource.CompletionActivation.NONE
|
|
||||||
# return GtkSource.CompletionActivation.USER_REQUESTED
|
|
||||||
return GtkSource.CompletionActivation.INTERACTIVE
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from libs.event_factory import Code_Event_Types
|
|
||||||
|
|
||||||
from core.widgets.code.completion_providers.provider_response_cache_base import ProviderResponseCacheBase
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ProviderResponseCache(ProviderResponseCacheBase):
|
|
||||||
def __init__(self):
|
|
||||||
super(ProviderResponseCache, self).__init__()
|
|
||||||
|
|
||||||
self.matchers: dict = {}
|
|
||||||
|
|
||||||
|
|
||||||
def process_file_load(self, event: Code_Event_Types.AddedNewFileEvent):
|
|
||||||
...
|
|
||||||
|
|
||||||
def process_file_close(self, event: Code_Event_Types.RemovedFileEvent):
|
|
||||||
...
|
|
||||||
|
|
||||||
def process_file_save(self, event: Code_Event_Types.SavedFileEvent):
|
|
||||||
...
|
|
||||||
|
|
||||||
def process_file_change(self, event: Code_Event_Types.TextChangedEvent):
|
|
||||||
...
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Words Completer",
|
|
||||||
"author": "ITDominator",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"support": "",
|
|
||||||
"requests": {}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
gi.require_version('Gtk', '3.0')
|
|
||||||
from gi.repository import Gtk
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from libs.event_factory import Event_Factory, Code_Event_Types
|
|
||||||
|
|
||||||
from plugins.plugin_types import PluginCode
|
|
||||||
|
|
||||||
from .provider import Provider
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Plugin(PluginCode):
|
|
||||||
def __init__(self):
|
|
||||||
super(Plugin, self).__init__()
|
|
||||||
|
|
||||||
self.provider: Provider = None
|
|
||||||
|
|
||||||
|
|
||||||
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
|
||||||
...
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
self.provider = Provider()
|
|
||||||
|
|
||||||
event = Event_Factory.create_event(
|
|
||||||
"register_provider",
|
|
||||||
provider_name = "Words Completer",
|
|
||||||
provider = self.provider,
|
|
||||||
language_ids = []
|
|
||||||
)
|
|
||||||
self.emit_to("completion", event)
|
|
||||||
|
|
||||||
def unload(self):
|
|
||||||
event = Event_Factory.create_event(
|
|
||||||
"unregister_provider",
|
|
||||||
provider_name = "Words Completer"
|
|
||||||
)
|
|
||||||
self.emit_to("completion", event)
|
|
||||||
|
|
||||||
self.provider = None
|
|
||||||
del self.provider
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
...
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
import re
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
gi.require_version('GtkSource', '4')
|
|
||||||
|
|
||||||
from gi.repository import GObject
|
|
||||||
from gi.repository import GtkSource
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from .provider_response_cache import ProviderResponseCache
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Provider(GObject.GObject, GtkSource.CompletionProvider):
|
|
||||||
"""
|
|
||||||
This is a Words Completion Provider.
|
|
||||||
# NOTE: used information from here --> https://warroom.rsmus.com/do-that-auto-complete/
|
|
||||||
"""
|
|
||||||
__gtype_name__ = 'WordsCompletionProvider'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(Provider, self).__init__()
|
|
||||||
|
|
||||||
self.response_cache: ProviderResponseCache = ProviderResponseCache()
|
|
||||||
|
|
||||||
|
|
||||||
def do_get_name(self):
|
|
||||||
return 'Words Completion'
|
|
||||||
|
|
||||||
def do_match(self, context):
|
|
||||||
# Note: If provider is in interactive activation then need to check
|
|
||||||
# view focus as otherwise non focus views start trying to grab it.
|
|
||||||
completion = context.get_property("completion")
|
|
||||||
if not completion.get_view().has_focus(): return
|
|
||||||
|
|
||||||
word = self.response_cache.get_word(context)
|
|
||||||
if not word or len(word) < 2: return False
|
|
||||||
|
|
||||||
iter = self.response_cache.get_iter_correctly(context)
|
|
||||||
iter.backward_char()
|
|
||||||
ch = iter.get_char()
|
|
||||||
# NOTE: Look to re-add or apply supprting logic to use spaces
|
|
||||||
# As is it slows down the editor in certain contexts...
|
|
||||||
# if not (ch in ('_', '.', ' ') or ch.isalnum()):
|
|
||||||
if not (ch in ('_', '.') or ch.isalnum()):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def do_get_priority(self):
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def do_activate_proposal(self, proposal, iter_):
|
|
||||||
buffer = iter_.get_buffer()
|
|
||||||
# Note: Flag mostly intended for SourceViewsMultiInsertState
|
|
||||||
# to insure marker processes inserted text correctly.
|
|
||||||
buffer.is_processing_completion = True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def do_get_activation(self):
|
|
||||||
""" The context for when a provider will show results """
|
|
||||||
# return GtkSource.CompletionActivation.NONE
|
|
||||||
# return GtkSource.CompletionActivation.USER_REQUESTED
|
|
||||||
return GtkSource.CompletionActivation.INTERACTIVE
|
|
||||||
|
|
||||||
def do_populate(self, context):
|
|
||||||
word = self.response_cache.get_word(context)
|
|
||||||
results = self.response_cache.filter_with_context(context)
|
|
||||||
# results = self.response_cache.filter(word)
|
|
||||||
|
|
||||||
# if not results:
|
|
||||||
# results = self.response_cache.filter_with_context(context)
|
|
||||||
|
|
||||||
proposals = []
|
|
||||||
for entry in results:
|
|
||||||
proposals.append(
|
|
||||||
self.response_cache.create_completion_item(
|
|
||||||
entry["label"],
|
|
||||||
entry["text"],
|
|
||||||
entry["info"]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
context.add_proposals(self, proposals, True)
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
gi.require_version('GtkSource', '4')
|
|
||||||
|
|
||||||
from gi.repository import GLib
|
|
||||||
from gi.repository import GtkSource
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from libs.event_factory import Code_Event_Types
|
|
||||||
|
|
||||||
from core.widgets.code.completion_providers.provider_response_cache_base import ProviderResponseCacheBase
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ProviderResponseCache(ProviderResponseCacheBase):
|
|
||||||
def __init__(self):
|
|
||||||
super(ProviderResponseCache, self).__init__()
|
|
||||||
|
|
||||||
self.matchers: dict = {}
|
|
||||||
self._temp_timeout_id: int = None
|
|
||||||
|
|
||||||
|
|
||||||
def process_file_load(self, event: Code_Event_Types.AddedNewFileEvent):
|
|
||||||
buffer = event.file.buffer
|
|
||||||
with ThreadPoolExecutor(max_workers = 1) as executor:
|
|
||||||
executor.submit(self._handle_change, buffer)
|
|
||||||
|
|
||||||
def process_file_close(self, event: Code_Event_Types.RemovedFileEvent):
|
|
||||||
self.matchers[event.file.buffer] = set()
|
|
||||||
del self.matchers[event.file.buffer]
|
|
||||||
|
|
||||||
def process_file_save(self, event: Code_Event_Types.SavedFileEvent):
|
|
||||||
...
|
|
||||||
|
|
||||||
def process_file_change(self, event: Code_Event_Types.TextChangedEvent):
|
|
||||||
buffer = event.file.buffer
|
|
||||||
self._clear_temp_delay()
|
|
||||||
self._set_temp_delay(buffer)
|
|
||||||
|
|
||||||
def _clear_temp_delay(self):
|
|
||||||
if self._temp_timeout_id:
|
|
||||||
GLib.source_remove(self._temp_timeout_id)
|
|
||||||
|
|
||||||
def _set_temp_delay(self, buffer):
|
|
||||||
def run_refresh_update(buffer):
|
|
||||||
with ThreadPoolExecutor(max_workers = 1) as executor:
|
|
||||||
executor.submit(self._handle_change, buffer)
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
self._temp_timeout_id = GLib.timeout_add(1500, run_refresh_update, buffer)
|
|
||||||
|
|
||||||
def _handle_change(self, buffer):
|
|
||||||
start_itr = buffer.get_start_iter()
|
|
||||||
end_itr = buffer.get_end_iter()
|
|
||||||
data = buffer.get_text(start_itr, end_itr, False)
|
|
||||||
|
|
||||||
if not data:
|
|
||||||
GLib.idle_add(self.load_empty_set, buffer)
|
|
||||||
return
|
|
||||||
|
|
||||||
if not buffer in self.matchers:
|
|
||||||
GLib.idle_add(self.load_as_new_set, buffer, data)
|
|
||||||
return
|
|
||||||
|
|
||||||
new_words = self.get_all_words(data)
|
|
||||||
GLib.idle_add(self.load_into_set, buffer, new_words)
|
|
||||||
|
|
||||||
def filter(self, word: str) -> list[dict]:
|
|
||||||
response: list[dict] = []
|
|
||||||
|
|
||||||
for entry in self.matchers:
|
|
||||||
if not word in entry: continue
|
|
||||||
data = self.matchers[entry]
|
|
||||||
response.append(data)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
def filter_with_context(self, context: GtkSource.CompletionContext) -> list[dict]:
|
|
||||||
buffer = self.get_iter_correctly(context).get_buffer()
|
|
||||||
word = self.get_word(context).rstrip()
|
|
||||||
|
|
||||||
if not buffer in self.matchers:
|
|
||||||
self.matchers[buffer] = set()
|
|
||||||
|
|
||||||
response: list[dict] = []
|
|
||||||
for entry in self.matchers[buffer]:
|
|
||||||
if not entry.rstrip().startswith(word): continue
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"label": entry,
|
|
||||||
"text": entry,
|
|
||||||
"info": ""
|
|
||||||
}
|
|
||||||
|
|
||||||
response.append(data)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
def load_empty_set(self, buffer):
|
|
||||||
self.matchers[buffer] = set()
|
|
||||||
|
|
||||||
def load_into_set(self, buffer, new_words):
|
|
||||||
# self.matchers[buffer].update(new_words)
|
|
||||||
self.matchers[buffer] = new_words
|
|
||||||
|
|
||||||
def load_as_new_set(self, buffer, data):
|
|
||||||
self.matchers[buffer] = self.get_all_words(data)
|
|
||||||
|
|
||||||
def get_all_words(self, data: str):
|
|
||||||
words = set()
|
|
||||||
|
|
||||||
def is_word_char(c):
|
|
||||||
return c.isalnum() or c == '_'
|
|
||||||
|
|
||||||
size = len(data)
|
|
||||||
i = 0
|
|
||||||
|
|
||||||
while i < size:
|
|
||||||
# Skip non-word characters
|
|
||||||
while i < size and not is_word_char(data[i]):
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
start = i
|
|
||||||
# Consume word characters
|
|
||||||
while i < size and is_word_char(data[i]):
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
word = data[start:i]
|
|
||||||
if not word: continue
|
|
||||||
words.add(word)
|
|
||||||
|
|
||||||
return words
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Plugin Module
|
|
||||||
"""
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Plugin Package
|
|
||||||
"""
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Extend Source View Menu",
|
|
||||||
"author": "ITDominator",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"support": "",
|
|
||||||
"requests": {}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from libs.event_factory import Event_Factory, Code_Event_Types
|
|
||||||
|
|
||||||
from plugins.plugin_types import PluginCode
|
|
||||||
|
|
||||||
from .source_view_menu import extend_source_view_menu
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Plugin(PluginCode):
|
|
||||||
def __init__(self):
|
|
||||||
super(Plugin, self).__init__()
|
|
||||||
|
|
||||||
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
|
||||||
if isinstance(event, Code_Event_Types.PopulateSourceViewPopupEvent):
|
|
||||||
extend_source_view_menu(event.buffer, event.menu)
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
def unload(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
...
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
|
|
||||||
gi.require_version('Gtk', '3.0')
|
|
||||||
|
|
||||||
from gi.repository import Gtk
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def on_case_handle(menuitem, buffer, action):
|
|
||||||
start_itr, \
|
|
||||||
end_itr = buffer.get_selection_bounds()
|
|
||||||
data = buffer.get_text(start_itr, end_itr, False)
|
|
||||||
text = data
|
|
||||||
|
|
||||||
if action == "on_all_upper":
|
|
||||||
text = data.upper()
|
|
||||||
elif action == "on_all_lower":
|
|
||||||
text = data.lower()
|
|
||||||
elif action == "on_invert":
|
|
||||||
text = data.swapcase()
|
|
||||||
elif action == "on_title":
|
|
||||||
text = data.title()
|
|
||||||
elif action == "on_title_strip":
|
|
||||||
text = data.title().replace("-", "").replace("_", "").replace(" ", "")
|
|
||||||
|
|
||||||
buffer.begin_user_action()
|
|
||||||
buffer.delete(start_itr, end_itr)
|
|
||||||
buffer.insert(start_itr, text)
|
|
||||||
buffer.end_user_action()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def extend_source_view_menu(buffer, menu):
|
|
||||||
if not buffer.get_selection_bounds(): return
|
|
||||||
|
|
||||||
for child in menu.get_children():
|
|
||||||
if not child.get_label() == "C_hange Case": continue
|
|
||||||
menu.remove(child)
|
|
||||||
|
|
||||||
change_case_item = Gtk.MenuItem(label = "Change Case")
|
|
||||||
|
|
||||||
case_menu = Gtk.Menu()
|
|
||||||
au_case_item = Gtk.MenuItem(label = "All Upper Case")
|
|
||||||
al_case_item = Gtk.MenuItem(label = "All Lower Case")
|
|
||||||
inver_case_item = Gtk.MenuItem(label = "Invert Case")
|
|
||||||
title_case_item = Gtk.MenuItem(label = "Title Case")
|
|
||||||
title_strip_case_item = Gtk.MenuItem(label = "Title Strip Case")
|
|
||||||
|
|
||||||
au_case_item.connect("activate", on_case_handle, buffer, "on_all_upper")
|
|
||||||
al_case_item.connect("activate", on_case_handle, buffer, "on_all_lower")
|
|
||||||
inver_case_item.connect("activate", on_case_handle, buffer, "on_invert")
|
|
||||||
title_case_item.connect("activate", on_case_handle, buffer, "on_title")
|
|
||||||
title_strip_case_item.connect("activate", on_case_handle, buffer, "on_title_strip")
|
|
||||||
|
|
||||||
case_menu.append(au_case_item)
|
|
||||||
case_menu.append(al_case_item)
|
|
||||||
case_menu.append(inver_case_item)
|
|
||||||
case_menu.append(title_case_item)
|
|
||||||
case_menu.append(title_strip_case_item)
|
|
||||||
change_case_item.set_submenu(case_menu)
|
|
||||||
|
|
||||||
menu.append(change_case_item)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Pligin Module
|
|
||||||
"""
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Pligin Package
|
|
||||||
"""
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "File State Watcher",
|
|
||||||
"author": "ITDominator",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"support": "",
|
|
||||||
"requests": {}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from libs.event_factory import Event_Factory, Code_Event_Types
|
|
||||||
|
|
||||||
from plugins.plugin_types import PluginCode
|
|
||||||
|
|
||||||
from .watcher_checks import *
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Plugin(PluginCode):
|
|
||||||
def __init__(self):
|
|
||||||
super(Plugin, self).__init__()
|
|
||||||
|
|
||||||
|
|
||||||
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
|
||||||
if not isinstance(event, Code_Event_Types.FocusedViewEvent): return
|
|
||||||
event = Event_Factory.create_event(
|
|
||||||
"get_file", buffer = event.view.get_buffer()
|
|
||||||
)
|
|
||||||
self.emit_to("files", event)
|
|
||||||
|
|
||||||
file = event.response
|
|
||||||
if file.ftype == "buffer": return
|
|
||||||
|
|
||||||
file.check_file_on_disk()
|
|
||||||
|
|
||||||
if file.is_deleted():
|
|
||||||
file_is_deleted(file, self.emit)
|
|
||||||
elif file.is_externally_modified():
|
|
||||||
file_is_externally_modified(file, self.emit)
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
def unload(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
...
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
gi.require_version('Gtk', '3.0')
|
|
||||||
from gi.repository import Gtk
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from libs.event_factory import Event_Factory, Code_Event_Types
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def ask_yes_no(message):
|
|
||||||
dialog = Gtk.MessageDialog(
|
|
||||||
parent = None,
|
|
||||||
flags = 0,
|
|
||||||
message_type = Gtk.MessageType.QUESTION,
|
|
||||||
buttons = Gtk.ButtonsType.YES_NO,
|
|
||||||
text = message,
|
|
||||||
)
|
|
||||||
dialog.set_title("Confirm")
|
|
||||||
|
|
||||||
response = dialog.run()
|
|
||||||
dialog.destroy()
|
|
||||||
|
|
||||||
return response == Gtk.ResponseType.YES
|
|
||||||
|
|
||||||
|
|
||||||
def file_is_deleted(file, emit):
|
|
||||||
file.was_deleted = True
|
|
||||||
event = Event_Factory.create_event(
|
|
||||||
"file_externally_deleted",
|
|
||||||
file = file,
|
|
||||||
buffer = file.buffer
|
|
||||||
)
|
|
||||||
emit(event)
|
|
||||||
|
|
||||||
|
|
||||||
def file_is_externally_modified(file, emit):
|
|
||||||
event = Event_Factory.create_event(
|
|
||||||
"file_externally_modified",
|
|
||||||
file = file,
|
|
||||||
buffer = file.buffer
|
|
||||||
)
|
|
||||||
emit(event)
|
|
||||||
|
|
||||||
if not file.buffer.get_modified():
|
|
||||||
file.reload()
|
|
||||||
return
|
|
||||||
|
|
||||||
result = ask_yes_no("File has been externally modified. Reload?")
|
|
||||||
if not result: return
|
|
||||||
|
|
||||||
file.reload()
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Pligin Module
|
|
||||||
"""
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Pligin Package
|
|
||||||
"""
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Prettify JSON",
|
|
||||||
"author": "ITDominator",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"support": "",
|
|
||||||
"requests": {}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
|
|
||||||
gi.require_version('Gtk', '3.0')
|
|
||||||
|
|
||||||
from gi.repository import Gtk
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from libs.event_factory import Event_Factory, Code_Event_Types
|
|
||||||
|
|
||||||
from plugins.plugin_types import PluginCode
|
|
||||||
|
|
||||||
from .prettify_json import add_prettify_json
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Plugin(PluginCode):
|
|
||||||
def __init__(self):
|
|
||||||
super(Plugin, self).__init__()
|
|
||||||
|
|
||||||
|
|
||||||
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
|
||||||
if isinstance(event, Code_Event_Types.PopulateSourceViewPopupEvent):
|
|
||||||
language = event.buffer.get_language()
|
|
||||||
if not language: return
|
|
||||||
|
|
||||||
if "json" == language.get_id():
|
|
||||||
add_prettify_json(event.buffer, event.menu)
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
def unload(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
...
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
|
|
||||||
gi.require_version('Gtk', '3.0')
|
|
||||||
|
|
||||||
from gi.repository import Gtk
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def add_prettify_json(buffer, menu):
|
|
||||||
def on_prettify_json(menuitem, buffer):
|
|
||||||
start_itr, \
|
|
||||||
end_itr = buffer.get_start_iter(), buffer.get_end_iter()
|
|
||||||
data = buffer.get_text(start_itr, end_itr, False)
|
|
||||||
text = json.dumps(json.loads(data), separators = (',', ':'), indent = 4)
|
|
||||||
|
|
||||||
buffer.begin_user_action()
|
|
||||||
buffer.delete(start_itr, end_itr)
|
|
||||||
buffer.insert(start_itr, text)
|
|
||||||
buffer.end_user_action()
|
|
||||||
|
|
||||||
item = Gtk.MenuItem(label = "Prettify JSON")
|
|
||||||
item.connect("activate", on_prettify_json, buffer)
|
|
||||||
menu.append(item)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Pligin Module
|
|
||||||
"""
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Pligin Package
|
|
||||||
"""
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
{
|
|
||||||
"info": "https://download.eclipse.org/jdtls/",
|
|
||||||
"info-init-options": "https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line",
|
|
||||||
"info-import-build": "https://www.javahotchocolate.com/tutorials/build-path.html",
|
|
||||||
"info-external-class-paths": "https://github.com/eclipse-jdtls/eclipse.jdt.ls/issues/3291",
|
|
||||||
"link": "https://download.eclipse.org/jdtls/milestones/?d",
|
|
||||||
"command": "lsp-ws-proxy --listen 4114 -- jdtls",
|
|
||||||
"alt-command": "lsp-ws-proxy -- jdtls",
|
|
||||||
"alt-command2": "java-language-server",
|
|
||||||
"socket": "ws://127.0.0.1:9999/java",
|
|
||||||
"socket-two": "ws://127.0.0.1:9999/?name=jdtls",
|
|
||||||
"alt-socket": "ws://127.0.0.1:9999/?name=java-language-server",
|
|
||||||
"initialization-options": {
|
|
||||||
"bundles": [
|
|
||||||
"intellicode-core.jar"
|
|
||||||
],
|
|
||||||
"workspaceFolders": [
|
|
||||||
"file://{workspace.folder}"
|
|
||||||
],
|
|
||||||
"extendedClientCapabilities": {
|
|
||||||
"classFileContentsSupport": true,
|
|
||||||
"executeClientCommandSupport": false
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"java": {
|
|
||||||
"autobuild": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"jdt": {
|
|
||||||
"ls": {
|
|
||||||
"javac": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"java": {
|
|
||||||
"home": "{user.home}/Portable_Apps/sdks/javasdk/jdk-22.0.2"
|
|
||||||
},
|
|
||||||
"lombokSupport": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"protobufSupport":{
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"androidSupport": {
|
|
||||||
"enabled": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"configuration": {
|
|
||||||
"updateBuildConfiguration": "automatic",
|
|
||||||
"maven": {
|
|
||||||
"userSettings": "{user.home}/.config/jdtls/settings.xml",
|
|
||||||
"globalSettings": "{user.home}/.config/jdtls/settings.xml"
|
|
||||||
},
|
|
||||||
"runtimes": [
|
|
||||||
{
|
|
||||||
"name": "JavaSE-17",
|
|
||||||
"path": "/usr/lib/jvm/java-17-openjdk",
|
|
||||||
"javadoc": "https://docs.oracle.com/en/java/javase/17/docs/api/",
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "JavaSE-22",
|
|
||||||
"path": "{user.home}/Portable_Apps/sdks/javasdk/jdk-22.0.2",
|
|
||||||
"javadoc": "https://docs.oracle.com/en/java/javase/22/docs/api/",
|
|
||||||
"default": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"classPath": [
|
|
||||||
"{user.home}/.config/jdtls/m2/repository/**/*-sources.jar",
|
|
||||||
"lib/**/*-sources.jar"
|
|
||||||
],
|
|
||||||
"docPath": [
|
|
||||||
"{user.home}/.config/jdtls/m2/repository/**/*-javadoc.jar",
|
|
||||||
"lib/**/*-javadoc.jar"
|
|
||||||
],
|
|
||||||
"project": {
|
|
||||||
"encoding": "ignore",
|
|
||||||
"outputPath": "bin",
|
|
||||||
"referencedLibraries": [
|
|
||||||
"{user.home}/.config/jdtls/m2/repository/**/*.jar",
|
|
||||||
"lib/**/*.jar"
|
|
||||||
],
|
|
||||||
"importOnFirstTimeStartup": "automatic",
|
|
||||||
"importHint": true,
|
|
||||||
"resourceFilters": [
|
|
||||||
"node_modules",
|
|
||||||
"\\.git"
|
|
||||||
],
|
|
||||||
"sourcePaths": [
|
|
||||||
"src",
|
|
||||||
"{user.home}/.config/jdtls/m2/repository/**/*.jar"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"sources": {
|
|
||||||
"organizeImports": {
|
|
||||||
"starThreshold": 99,
|
|
||||||
"staticStarThreshold": 99
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"imports": {
|
|
||||||
"gradle": {
|
|
||||||
"wrapper": {
|
|
||||||
"checksums": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"import": {
|
|
||||||
"maven": {
|
|
||||||
"enabled": true,
|
|
||||||
"offline": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"disableTestClasspathFlag": false
|
|
||||||
},
|
|
||||||
"gradle": {
|
|
||||||
"enabled": false,
|
|
||||||
"wrapper": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"version": "",
|
|
||||||
"home": "abs(static/gradle-7.3.3)",
|
|
||||||
"java": {
|
|
||||||
"home": "abs(static/launch_jres/17.0.6-linux-x86_64)"
|
|
||||||
},
|
|
||||||
"offline": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"arguments": [],
|
|
||||||
"jvmArguments": [],
|
|
||||||
"user": {
|
|
||||||
"home": ""
|
|
||||||
},
|
|
||||||
"annotationProcessing": {
|
|
||||||
"enabled": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"exclusions": [
|
|
||||||
"**/node_modules/**",
|
|
||||||
"**/.metadata/**",
|
|
||||||
"**/archetype-resources/**",
|
|
||||||
"**/META-INF/maven/**"
|
|
||||||
],
|
|
||||||
"generatesMetadataFilesAtProjectRoot": false
|
|
||||||
},
|
|
||||||
"maven": {
|
|
||||||
"downloadSources": true,
|
|
||||||
"updateSnapshots": true
|
|
||||||
},
|
|
||||||
"silentNotification": true,
|
|
||||||
"contentProvider": {
|
|
||||||
"preferred": "fernflower"
|
|
||||||
},
|
|
||||||
"signatureHelp": {
|
|
||||||
"enabled": true,
|
|
||||||
"description": {
|
|
||||||
"enabled": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"completion": {
|
|
||||||
"enabled": true,
|
|
||||||
"engine": "ecj",
|
|
||||||
"matchCase": "firstletter",
|
|
||||||
"maxResults": 25,
|
|
||||||
"guessMethodArguments": true,
|
|
||||||
"lazyResolveTextEdit": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"postfix": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"favoriteStaticMembers": [
|
|
||||||
"org.junit.Assert.*",
|
|
||||||
"org.junit.Assume.*",
|
|
||||||
"org.junit.jupiter.api.Assertions.*",
|
|
||||||
"org.junit.jupiter.api.Assumptions.*",
|
|
||||||
"org.junit.jupiter.api.DynamicContainer.*",
|
|
||||||
"org.junit.jupiter.api.DynamicTest.*"
|
|
||||||
],
|
|
||||||
"importOrder": [
|
|
||||||
"#",
|
|
||||||
"java",
|
|
||||||
"javax",
|
|
||||||
"org",
|
|
||||||
"com"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"references": {
|
|
||||||
"includeAccessors": true,
|
|
||||||
"includeDecompiledSources": true
|
|
||||||
},
|
|
||||||
"codeGeneration": {
|
|
||||||
"toString": {
|
|
||||||
"template": "${object.className}{${member.name()}=${member.value}, ${otherMembers}}"
|
|
||||||
},
|
|
||||||
"insertionLocation": "afterCursor",
|
|
||||||
"useBlocks": true
|
|
||||||
},
|
|
||||||
"implementationsCodeLens": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"referencesCodeLens": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"progressReports": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"saveActions": {
|
|
||||||
"organizeImports": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Java LSP Client",
|
|
||||||
"author": "ITDominator",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"support": "",
|
|
||||||
"autoload": false,
|
|
||||||
"requests": {}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
from os import path
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
|
|
||||||
from gi.repository import GLib
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from libs.event_factory import Event_Factory, Code_Event_Types
|
|
||||||
|
|
||||||
from plugins.plugin_types import PluginCode
|
|
||||||
|
|
||||||
from .response_handler import JavaHandler
|
|
||||||
|
|
||||||
|
|
||||||
class Plugin(PluginCode):
|
|
||||||
def __init__(self):
|
|
||||||
super(Plugin, self).__init__()
|
|
||||||
|
|
||||||
|
|
||||||
def _controller_message(self, event: Code_Event_Types.CodeEvent):
|
|
||||||
...
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
dirPth = path.dirname( path.realpath(__file__) )
|
|
||||||
with open(f"{dirPth}/config/lsp-server-config.json", "r") as f:
|
|
||||||
config = f.read()
|
|
||||||
event = Event_Factory.create_event("register_lsp_client",
|
|
||||||
lang_id = "java",
|
|
||||||
lang_config = config,
|
|
||||||
handler = JavaHandler
|
|
||||||
)
|
|
||||||
self.emit_to("lsp_manager", event)
|
|
||||||
|
|
||||||
def unload(self):
|
|
||||||
event = Event_Factory.create_event("unregister_lsp_client",
|
|
||||||
lang_id = "java"
|
|
||||||
)
|
|
||||||
self.emit_to("lsp_manager", event)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
...
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from .java import JavaHandler
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
gi.require_version('GtkSource', '4')
|
|
||||||
|
|
||||||
from gi.repository import GtkSource
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from libs.event_factory import Event_Factory, Code_Event_Types
|
|
||||||
|
|
||||||
from lsp_manager.response_handlers.default import DefaultHandler
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class JavaHandler(DefaultHandler):
|
|
||||||
"""Java-specific: overrides definition, handles classFileContents."""
|
|
||||||
|
|
||||||
def handle(self, method: str, response, controller):
|
|
||||||
match method:
|
|
||||||
case "textDocument/definition":
|
|
||||||
self._handle_definition(response, controller)
|
|
||||||
case "java/classFileContents":
|
|
||||||
self._handle_class_file_contents(response)
|
|
||||||
case _:
|
|
||||||
super().handle(method, response, controller)
|
|
||||||
|
|
||||||
def _handle_definition(self, response, controller):
|
|
||||||
if not response: return
|
|
||||||
|
|
||||||
uri = response[0]["uri"]
|
|
||||||
if "jdt://" in uri:
|
|
||||||
controller._lsp_java_class_file_contents(uri)
|
|
||||||
return
|
|
||||||
|
|
||||||
self._prompt_goto_request(uri, response[0]["range"])
|
|
||||||
|
|
||||||
def _handle_class_file_contents(self, text: str):
|
|
||||||
event = Event_Factory.create_event("get_active_view")
|
|
||||||
self.emit_to("source_views", event)
|
|
||||||
|
|
||||||
view = event.response
|
|
||||||
file = view.command.exec("new_file")
|
|
||||||
buffer = view.get_buffer()
|
|
||||||
itr = buffer.get_iter_at_mark(buffer.get_insert())
|
|
||||||
lm = GtkSource.LanguageManager.get_default()
|
|
||||||
language = lm.get_language("java")
|
|
||||||
file.ftype = "java"
|
|
||||||
|
|
||||||
buffer.set_language(language)
|
|
||||||
buffer.insert(itr, text, -1)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Pligin Module
|
|
||||||
"""
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Pligin Package
|
|
||||||
"""
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
LSP Clients Module
|
|
||||||
"""
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
import threading
|
|
||||||
from os import path
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
import gi
|
|
||||||
from gi.repository import GLib
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from ..dto.code.lsp.lsp_messages import get_message_str
|
|
||||||
from ..dto.code.lsp.lsp_message_structs import \
|
|
||||||
LSPResponseTypes, ClientRequest, ClientNotification
|
|
||||||
from .lsp_client_websocket import LSPClientWebsocket
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class LSPClient(LSPClientWebsocket):
|
|
||||||
def __init__(self):
|
|
||||||
super(LSPClient, self).__init__()
|
|
||||||
|
|
||||||
# https://github.com/microsoft/multilspy/tree/main/src/multilspy/language_servers
|
|
||||||
# initialize-params-slim.json was created off of jedi_language_server one
|
|
||||||
# self._init_params = settings_manager.get_lsp_init_data()
|
|
||||||
|
|
||||||
self._language: str = ""
|
|
||||||
self._init_params: dict = {}
|
|
||||||
self._event_history: dict[int, str] = {}
|
|
||||||
|
|
||||||
try:
|
|
||||||
_USER_HOME = path.expanduser('~')
|
|
||||||
_SCRIPT_PTH = path.dirname( path.realpath(__file__) )
|
|
||||||
_LSP_INIT_CONFIG = f"{_SCRIPT_PTH}/../configs/initialize-params-slim.json"
|
|
||||||
|
|
||||||
with open(_LSP_INIT_CONFIG) as file:
|
|
||||||
data = file.read().replace("{user.home}", _USER_HOME)
|
|
||||||
self._init_params = json.loads(data)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error( f"LSP Controller: {_LSP_INIT_CONFIG}\n\t\t{repr(e)}" )
|
|
||||||
|
|
||||||
self._message_id: int = -1
|
|
||||||
self._socket = None
|
|
||||||
self.read_lock = threading.Lock()
|
|
||||||
self.write_lock = threading.Lock()
|
|
||||||
|
|
||||||
def set_language(self, language: str):
|
|
||||||
self._language = language
|
|
||||||
|
|
||||||
def set_socket(self, socket: str):
|
|
||||||
self._socket = socket
|
|
||||||
|
|
||||||
def unset_socket(self):
|
|
||||||
self._socket = None
|
|
||||||
|
|
||||||
def send_notification(self, method: str, params: dict = {}):
|
|
||||||
self._send_message( ClientNotification(method, params) )
|
|
||||||
|
|
||||||
def send_request(self, method: str, params: dict = {}):
|
|
||||||
self._message_id += 1
|
|
||||||
self._event_history[self._message_id] = method
|
|
||||||
self._send_message( ClientRequest(self._message_id, method, params) )
|
|
||||||
|
|
||||||
def get_event_by_id(self, message_id: int) -> str:
|
|
||||||
if not message_id in self._event_history: return
|
|
||||||
return self._event_history[message_id]
|
|
||||||
|
|
||||||
def handle_lsp_response(self, lsp_response: LSPResponseTypes):
|
|
||||||
raise NotImplementedError
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from ..dto.code.lsp.lsp_message_structs import ClientRequest, ClientNotification
|
|
||||||
|
|
||||||
from .lsp_client_events import LSPClientEvents
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class LSPClientBase(LSPClientEvents):
|
|
||||||
def _send_message(self, data: ClientRequest or ClientNotification):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def start_client(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def stop_client(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from ..dto.code.lsp.lsp_messages import get_message_obj
|
|
||||||
from ..dto.code.lsp.lsp_messages import didopen_notification
|
|
||||||
from ..dto.code.lsp.lsp_messages import didsave_notification
|
|
||||||
from ..dto.code.lsp.lsp_messages import didclose_notification
|
|
||||||
from ..dto.code.lsp.lsp_messages import didchange_notification
|
|
||||||
from ..dto.code.lsp.lsp_messages import completion_request
|
|
||||||
from ..dto.code.lsp.lsp_messages import definition_request
|
|
||||||
from ..dto.code.lsp.lsp_messages import references_request
|
|
||||||
from ..dto.code.lsp.lsp_messages import symbols_request
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class LSPClientEvents:
|
|
||||||
def send_initialize_message(self, init_ops: dict, workspace_file: str, workspace_uri: str):
|
|
||||||
folder_name = os.path.basename(workspace_file)
|
|
||||||
|
|
||||||
self._init_params["processId"] = None
|
|
||||||
self._init_params["rootPath"] = workspace_file
|
|
||||||
self._init_params["rootUri"] = workspace_uri
|
|
||||||
self._init_params["workspaceFolders"] = [
|
|
||||||
{
|
|
||||||
"name": folder_name,
|
|
||||||
"uri": workspace_uri
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
self._init_params["initializationOptions"] = init_ops
|
|
||||||
self.send_request("initialize", self._init_params)
|
|
||||||
|
|
||||||
def send_initialized_message(self):
|
|
||||||
self.send_notification("initialized")
|
|
||||||
|
|
||||||
def _lsp_did_open(self, data: dict):
|
|
||||||
method = "textDocument/didOpen"
|
|
||||||
params = didopen_notification["params"]
|
|
||||||
|
|
||||||
params["textDocument"]["uri"] = data["uri"]
|
|
||||||
params["textDocument"]["languageId"] = data["language_id"]
|
|
||||||
params["textDocument"]["text"] = data["text"]
|
|
||||||
|
|
||||||
self.send_notification( method, params )
|
|
||||||
|
|
||||||
def _lsp_did_save(self, data: dict):
|
|
||||||
method = "textDocument/didSave"
|
|
||||||
params = didsave_notification["params"]
|
|
||||||
|
|
||||||
params["textDocument"]["uri"] = data["uri"]
|
|
||||||
params["text"] = data["text"]
|
|
||||||
|
|
||||||
self.send_notification( method, params )
|
|
||||||
|
|
||||||
def _lsp_did_close(self, data: dict):
|
|
||||||
method = "textDocument/didClose"
|
|
||||||
params = didclose_notification["params"]
|
|
||||||
|
|
||||||
params["textDocument"]["uri"] = data["uri"]
|
|
||||||
|
|
||||||
self.send_notification( method, params )
|
|
||||||
|
|
||||||
def _lsp_did_change(self, data: dict):
|
|
||||||
method = "textDocument/didChange"
|
|
||||||
params = didchange_notification["params"]
|
|
||||||
|
|
||||||
params["textDocument"]["uri"] = data["uri"]
|
|
||||||
params["textDocument"]["languageId"] = data["language_id"]
|
|
||||||
params["textDocument"]["version"] = data["version"]
|
|
||||||
|
|
||||||
contentChanges = params["contentChanges"][0]
|
|
||||||
contentChanges["text"] = data["text"]
|
|
||||||
|
|
||||||
self.send_notification( method, params )
|
|
||||||
|
|
||||||
# def _lsp_did_change(self, data: dict):
|
|
||||||
# method = "textDocument/didChange"
|
|
||||||
# params = didchange_notification_range["params"]
|
|
||||||
|
|
||||||
# params["textDocument"]["uri"] = data["uri"]
|
|
||||||
# params["textDocument"]["languageId"] = data["language_id"]
|
|
||||||
# params["textDocument"]["version"] = data["version"]
|
|
||||||
|
|
||||||
# contentChanges = params["contentChanges"][0]
|
|
||||||
# start = contentChanges["range"]["start"]
|
|
||||||
# end = contentChanges["range"]["end"]
|
|
||||||
# contentChanges["text"] = data["text"]
|
|
||||||
# start["line"] = data["line"]
|
|
||||||
# start["character"] = 0
|
|
||||||
# end["line"] = data["line"]
|
|
||||||
# end["character"] = data["column"]
|
|
||||||
|
|
||||||
# self.send_notification( method, params )
|
|
||||||
|
|
||||||
def _lsp_definition(self, data: dict):
|
|
||||||
method = "textDocument/definition"
|
|
||||||
params = definition_request["params"]
|
|
||||||
|
|
||||||
params["textDocument"]["uri"] = data["uri"]
|
|
||||||
params["textDocument"]["languageId"] = data["language_id"]
|
|
||||||
params["textDocument"]["version"] = data["version"]
|
|
||||||
params["position"]["line"] = data["line"]
|
|
||||||
params["position"]["character"] = data["column"]
|
|
||||||
|
|
||||||
self.send_request( method, params )
|
|
||||||
|
|
||||||
def _lsp_completion(self, data: dict):
|
|
||||||
method = "textDocument/completion"
|
|
||||||
params = completion_request["params"]
|
|
||||||
|
|
||||||
params["textDocument"]["uri"] = data["uri"]
|
|
||||||
params["textDocument"]["languageId"] = data["language_id"]
|
|
||||||
params["textDocument"]["version"] = data["version"]
|
|
||||||
params["position"]["line"] = data["line"]
|
|
||||||
params["position"]["character"] = data["column"]
|
|
||||||
|
|
||||||
self.send_request( method, params )
|
|
||||||
|
|
||||||
def _lsp_java_class_file_contents(self, uri: str):
|
|
||||||
method = "java/classFileContents"
|
|
||||||
params = {
|
|
||||||
"uri": uri
|
|
||||||
}
|
|
||||||
|
|
||||||
self.send_request( method, params )
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
from gi.repository import GLib
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
# from libs import websockets
|
|
||||||
from ..dto.code.lsp.lsp_messages import get_message_str, get_message_obj
|
|
||||||
from ..dto.code.lsp.lsp_message_structs import \
|
|
||||||
LSPResponseTypes, ClientRequest, ClientNotification, \
|
|
||||||
LSPResponseRequest, LSPResponseNotification, LSPIDResponseNotification
|
|
||||||
|
|
||||||
from .lsp_client_base import LSPClientBase
|
|
||||||
from .websocket_client import WebsocketClient
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class LSPClientWebsocket(LSPClientBase):
|
|
||||||
def _send_message(self, data: ClientRequest | ClientNotification):
|
|
||||||
if not data: return
|
|
||||||
|
|
||||||
message_str = get_message_str(data)
|
|
||||||
message_size = len(message_str)
|
|
||||||
message = f"Content-Length: {message_size}\r\n\r\n{message_str}"
|
|
||||||
|
|
||||||
logger.debug(f"Client: {message_str}")
|
|
||||||
self.ws_client.send(message_str)
|
|
||||||
|
|
||||||
def start_client(self):
|
|
||||||
self.ws_client = WebsocketClient()
|
|
||||||
self.ws_client.set_socket(self._socket)
|
|
||||||
self.ws_client.set_callback(self._monitor_lsp_response)
|
|
||||||
self.ws_client.start_client()
|
|
||||||
|
|
||||||
return self.ws_client
|
|
||||||
|
|
||||||
def stop_client(self):
|
|
||||||
if not hasattr(self, "ws_client"): return
|
|
||||||
self.ws_client.close_client()
|
|
||||||
|
|
||||||
def _monitor_lsp_response(self, data: dict | None):
|
|
||||||
if not data: return
|
|
||||||
|
|
||||||
message = get_message_obj(data)
|
|
||||||
keys = message.keys()
|
|
||||||
lsp_response = None
|
|
||||||
|
|
||||||
if "result" in keys:
|
|
||||||
lsp_response = LSPResponseRequest(**get_message_obj(data))
|
|
||||||
|
|
||||||
if "method" in keys:
|
|
||||||
lsp_response = LSPResponseNotification(**get_message_obj(data)) if not "id" in keys else LSPIDResponseNotification( **get_message_obj(data) )
|
|
||||||
|
|
||||||
if not lsp_response: return
|
|
||||||
|
|
||||||
GLib.idle_add(self.handle_lsp_response, lsp_response)
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
import json
|
|
||||||
import threading
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from ..libs import websocket
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class WebsocketClient:
|
|
||||||
def __init__(self):
|
|
||||||
self.ws = None
|
|
||||||
self._socket = None
|
|
||||||
self._connected = threading.Event()
|
|
||||||
|
|
||||||
|
|
||||||
def set_socket(self, socket: str):
|
|
||||||
self._socket = socket
|
|
||||||
|
|
||||||
def unset_socket(self):
|
|
||||||
self._socket = None
|
|
||||||
|
|
||||||
def send(self, message: str):
|
|
||||||
self.ws.send(message)
|
|
||||||
|
|
||||||
def on_message(self, ws, message: dict):
|
|
||||||
self.respond(message)
|
|
||||||
|
|
||||||
def on_error(self, ws, error: str):
|
|
||||||
logger.debug(f"WS Error: {error}")
|
|
||||||
|
|
||||||
def on_close(self, ws, close_status_code: int, close_msg: str):
|
|
||||||
logger.debug("WS Closed...")
|
|
||||||
|
|
||||||
def on_open(self, ws):
|
|
||||||
self._connected.set()
|
|
||||||
logger.debug("WS opened connection...")
|
|
||||||
|
|
||||||
def wait_for_connection(self, timeout: float = 5.0) -> bool:
|
|
||||||
return self._connected.wait(timeout)
|
|
||||||
|
|
||||||
def set_callback(self, callback: object):
|
|
||||||
self.respond = callback
|
|
||||||
|
|
||||||
def close_client(self):
|
|
||||||
self.ws.close()
|
|
||||||
|
|
||||||
@daemon_threaded
|
|
||||||
def start_client(self):
|
|
||||||
if not self._socket:
|
|
||||||
raise Exception("Socket address isn't set so cannot start WebsocketClient listener...")
|
|
||||||
|
|
||||||
# websocket.enableTrace(True)
|
|
||||||
self.ws = websocket.WebSocketApp(self._socket,
|
|
||||||
on_open = self.on_open,
|
|
||||||
on_message = self.on_message,
|
|
||||||
on_error = self.on_error,
|
|
||||||
on_close = self.on_close)
|
|
||||||
|
|
||||||
self.ws.run_forever(reconnect = 0.5)
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
{
|
|
||||||
"_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize",
|
|
||||||
"processId": "os.getpid()",
|
|
||||||
"clientInfo": {
|
|
||||||
"name": "LSP Manager",
|
|
||||||
"version": "0.0.1"
|
|
||||||
},
|
|
||||||
"locale": "en",
|
|
||||||
"rootPath": "repository_absolute_path",
|
|
||||||
"rootUri": "pathlib.Path(repository_absolute_path).as_uri()",
|
|
||||||
"capabilities": {
|
|
||||||
"textDocument": {
|
|
||||||
"completion": {
|
|
||||||
"dynamicRegistration": true,
|
|
||||||
"contextSupport": true,
|
|
||||||
"completionItem": {
|
|
||||||
"snippetSupport": false,
|
|
||||||
"commitCharactersSupport": true,
|
|
||||||
"documentationFormat": [
|
|
||||||
"markdown",
|
|
||||||
"plaintext"
|
|
||||||
],
|
|
||||||
"deprecatedSupport": true,
|
|
||||||
"preselectSupport": true,
|
|
||||||
"tagSupport": {
|
|
||||||
"valueSet": [
|
|
||||||
1
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"insertReplaceSupport": false,
|
|
||||||
"resolveSupport": {
|
|
||||||
"properties": [
|
|
||||||
"documentation",
|
|
||||||
"detail",
|
|
||||||
"additionalTextEdits"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"insertTextModeSupport": {
|
|
||||||
"valueSet": [
|
|
||||||
1,
|
|
||||||
2
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"labelDetailsSupport": true
|
|
||||||
},
|
|
||||||
"insertTextMode": 2,
|
|
||||||
"completionItemKind": {
|
|
||||||
"valueSet": [
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
3,
|
|
||||||
4,
|
|
||||||
5,
|
|
||||||
6,
|
|
||||||
7,
|
|
||||||
8,
|
|
||||||
9,
|
|
||||||
10,
|
|
||||||
11,
|
|
||||||
12,
|
|
||||||
13,
|
|
||||||
14,
|
|
||||||
15,
|
|
||||||
16,
|
|
||||||
17,
|
|
||||||
18,
|
|
||||||
19,
|
|
||||||
20,
|
|
||||||
21,
|
|
||||||
22,
|
|
||||||
23,
|
|
||||||
24,
|
|
||||||
25
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"completionList": {
|
|
||||||
"itemDefaults": [
|
|
||||||
"commitCharacters",
|
|
||||||
"editRange",
|
|
||||||
"insertTextFormat",
|
|
||||||
"insertTextMode"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"hover": {
|
|
||||||
"dynamicRegistration": true,
|
|
||||||
"contentFormat": [
|
|
||||||
"markdown",
|
|
||||||
"plaintext"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"signatureHelp": {
|
|
||||||
"dynamicRegistration": true,
|
|
||||||
"signatureInformation": {
|
|
||||||
"documentationFormat": [
|
|
||||||
"markdown",
|
|
||||||
"plaintext"
|
|
||||||
],
|
|
||||||
"parameterInformation": {
|
|
||||||
"labelOffsetSupport": true
|
|
||||||
},
|
|
||||||
"activeParameterSupport": true
|
|
||||||
},
|
|
||||||
"contextSupport": true
|
|
||||||
},
|
|
||||||
"definition": {
|
|
||||||
"dynamicRegistration": true,
|
|
||||||
"linkSupport": true
|
|
||||||
},
|
|
||||||
"references": {
|
|
||||||
"dynamicRegistration": true
|
|
||||||
},
|
|
||||||
"typeDefinition": {
|
|
||||||
"dynamicRegistration": true,
|
|
||||||
"linkSupport": true
|
|
||||||
},
|
|
||||||
"implementation": {
|
|
||||||
"dynamicRegistration": true,
|
|
||||||
"linkSupport": true
|
|
||||||
},
|
|
||||||
"colorProvider": {
|
|
||||||
"dynamicRegistration": true
|
|
||||||
},
|
|
||||||
"declaration": {
|
|
||||||
"dynamicRegistration": true,
|
|
||||||
"linkSupport": true
|
|
||||||
},
|
|
||||||
"callHierarchy": {
|
|
||||||
"dynamicRegistration": true
|
|
||||||
},
|
|
||||||
"inlayHint": {
|
|
||||||
"dynamicRegistration": true,
|
|
||||||
"resolveSupport": {
|
|
||||||
"properties": [
|
|
||||||
"tooltip",
|
|
||||||
"textEdits",
|
|
||||||
"label.tooltip",
|
|
||||||
"label.location",
|
|
||||||
"label.command"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"diagnostic": {
|
|
||||||
"dynamicRegistration": true,
|
|
||||||
"relatedDocumentSupport": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"trace": "verbose",
|
|
||||||
"workspaceFolders": "[\n {\n \"uri\": pathlib.Path(repository_absolute_path).as_uri(),\n \"name\": os.path.basename(repository_absolute_path),\n }\n ]"
|
|
||||||
}
|
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
{
|
|
||||||
"java": {
|
|
||||||
"info": "https://download.eclipse.org/jdtls/",
|
|
||||||
"info-init-options": "https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line",
|
|
||||||
"info-import-build": "https://www.javahotchocolate.com/tutorials/build-path.html",
|
|
||||||
"info-external-class-paths": "https://github.com/eclipse-jdtls/eclipse.jdt.ls/issues/3291",
|
|
||||||
"link": "https://download.eclipse.org/jdtls/milestones/?d",
|
|
||||||
"command": "lsp-ws-proxy --listen 4114 -- jdtls",
|
|
||||||
"alt-command": "lsp-ws-proxy -- jdtls",
|
|
||||||
"alt-command2": "java-language-server",
|
|
||||||
"socket": "ws://127.0.0.1:9999/java",
|
|
||||||
"socket-two": "ws://127.0.0.1:9999/?name=jdtls",
|
|
||||||
"alt-socket": "ws://127.0.0.1:9999/?name=java-language-server",
|
|
||||||
"initialization-options": {
|
|
||||||
"bundles": [
|
|
||||||
"intellicode-core.jar"
|
|
||||||
],
|
|
||||||
"workspaceFolders": [
|
|
||||||
"file://{workspace.folder}"
|
|
||||||
],
|
|
||||||
"extendedClientCapabilities": {
|
|
||||||
"classFileContentsSupport": true,
|
|
||||||
"executeClientCommandSupport": false
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"java": {
|
|
||||||
"autobuild": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"jdt": {
|
|
||||||
"ls": {
|
|
||||||
"javac": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"java": {
|
|
||||||
"home": "{user.home}/Portable_Apps/sdks/javasdk/jdk-22.0.2"
|
|
||||||
},
|
|
||||||
"lombokSupport": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"protobufSupport":{
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"androidSupport": {
|
|
||||||
"enabled": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"configuration": {
|
|
||||||
"updateBuildConfiguration": "automatic",
|
|
||||||
"maven": {
|
|
||||||
"userSettings": "{user.home}/.config/jdtls/settings.xml",
|
|
||||||
"globalSettings": "{user.home}/.config/jdtls/settings.xml"
|
|
||||||
},
|
|
||||||
"runtimes": [
|
|
||||||
{
|
|
||||||
"name": "JavaSE-17",
|
|
||||||
"path": "/usr/lib/jvm/java-17-openjdk",
|
|
||||||
"javadoc": "https://docs.oracle.com/en/java/javase/17/docs/api/",
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "JavaSE-22",
|
|
||||||
"path": "{user.home}/Portable_Apps/sdks/javasdk/jdk-22.0.2",
|
|
||||||
"javadoc": "https://docs.oracle.com/en/java/javase/22/docs/api/",
|
|
||||||
"default": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"classPath": [
|
|
||||||
"{user.home}/.config/jdtls/m2/repository/**/*-sources.jar",
|
|
||||||
"lib/**/*-sources.jar"
|
|
||||||
],
|
|
||||||
"docPath": [
|
|
||||||
"{user.home}/.config/jdtls/m2/repository/**/*-javadoc.jar",
|
|
||||||
"lib/**/*-javadoc.jar"
|
|
||||||
],
|
|
||||||
"project": {
|
|
||||||
"encoding": "ignore",
|
|
||||||
"outputPath": "bin",
|
|
||||||
"referencedLibraries": [
|
|
||||||
"{user.home}/.config/jdtls/m2/repository/**/*.jar",
|
|
||||||
"lib/**/*.jar"
|
|
||||||
],
|
|
||||||
"importOnFirstTimeStartup": "automatic",
|
|
||||||
"importHint": true,
|
|
||||||
"resourceFilters": [
|
|
||||||
"node_modules",
|
|
||||||
"\\.git"
|
|
||||||
],
|
|
||||||
"sourcePaths": [
|
|
||||||
"src",
|
|
||||||
"{user.home}/.config/jdtls/m2/repository/**/*.jar"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"sources": {
|
|
||||||
"organizeImports": {
|
|
||||||
"starThreshold": 99,
|
|
||||||
"staticStarThreshold": 99
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"imports": {
|
|
||||||
"gradle": {
|
|
||||||
"wrapper": {
|
|
||||||
"checksums": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"import": {
|
|
||||||
"maven": {
|
|
||||||
"enabled": true,
|
|
||||||
"offline": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"disableTestClasspathFlag": false
|
|
||||||
},
|
|
||||||
"gradle": {
|
|
||||||
"enabled": false,
|
|
||||||
"wrapper": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"version": "",
|
|
||||||
"home": "abs(static/gradle-7.3.3)",
|
|
||||||
"java": {
|
|
||||||
"home": "abs(static/launch_jres/17.0.6-linux-x86_64)"
|
|
||||||
},
|
|
||||||
"offline": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"arguments": [],
|
|
||||||
"jvmArguments": [],
|
|
||||||
"user": {
|
|
||||||
"home": ""
|
|
||||||
},
|
|
||||||
"annotationProcessing": {
|
|
||||||
"enabled": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"exclusions": [
|
|
||||||
"**/node_modules/**",
|
|
||||||
"**/.metadata/**",
|
|
||||||
"**/archetype-resources/**",
|
|
||||||
"**/META-INF/maven/**"
|
|
||||||
],
|
|
||||||
"generatesMetadataFilesAtProjectRoot": false
|
|
||||||
},
|
|
||||||
"maven": {
|
|
||||||
"downloadSources": true,
|
|
||||||
"updateSnapshots": true
|
|
||||||
},
|
|
||||||
"silentNotification": true,
|
|
||||||
"contentProvider": {
|
|
||||||
"preferred": "fernflower"
|
|
||||||
},
|
|
||||||
"signatureHelp": {
|
|
||||||
"enabled": true,
|
|
||||||
"description": {
|
|
||||||
"enabled": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"completion": {
|
|
||||||
"enabled": true,
|
|
||||||
"engine": "ecj",
|
|
||||||
"matchCase": "firstletter",
|
|
||||||
"maxResults": 25,
|
|
||||||
"guessMethodArguments": true,
|
|
||||||
"lazyResolveTextEdit": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"postfix": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"favoriteStaticMembers": [
|
|
||||||
"org.junit.Assert.*",
|
|
||||||
"org.junit.Assume.*",
|
|
||||||
"org.junit.jupiter.api.Assertions.*",
|
|
||||||
"org.junit.jupiter.api.Assumptions.*",
|
|
||||||
"org.junit.jupiter.api.DynamicContainer.*",
|
|
||||||
"org.junit.jupiter.api.DynamicTest.*"
|
|
||||||
],
|
|
||||||
"importOrder": [
|
|
||||||
"#",
|
|
||||||
"java",
|
|
||||||
"javax",
|
|
||||||
"org",
|
|
||||||
"com"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"references": {
|
|
||||||
"includeAccessors": true,
|
|
||||||
"includeDecompiledSources": true
|
|
||||||
},
|
|
||||||
"codeGeneration": {
|
|
||||||
"toString": {
|
|
||||||
"template": "${object.className}{${member.name()}=${member.value}, ${otherMembers}}"
|
|
||||||
},
|
|
||||||
"insertionLocation": "afterCursor",
|
|
||||||
"useBlocks": true
|
|
||||||
},
|
|
||||||
"implementationsCodeLens": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"referencesCodeLens": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"progressReports": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"saveActions": {
|
|
||||||
"organizeImports": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"python": {
|
|
||||||
"info": "https://github.com/python-lsp/python-lsp-server",
|
|
||||||
"command": "lsp-ws-proxy -- pylsp",
|
|
||||||
"alt-command": "pylsp",
|
|
||||||
"alt-command2": "lsp-ws-proxy --listen 4114 -- pylsp",
|
|
||||||
"alt-command3": "pylsp --ws --port 4114",
|
|
||||||
"socket": "ws://127.0.0.1:9999/python",
|
|
||||||
"socket-two": "ws://127.0.0.1:9999/?name=pylsp",
|
|
||||||
"initialization-options": {
|
|
||||||
"pylsp": {
|
|
||||||
"rope": {
|
|
||||||
"ropeFolder": "{user.home}/.config/newton/lsps/ropeproject"
|
|
||||||
},
|
|
||||||
"plugins": {
|
|
||||||
"ruff": {
|
|
||||||
"enabled": true,
|
|
||||||
"extendSelect": ["I"],
|
|
||||||
"lineLength": 80
|
|
||||||
},
|
|
||||||
"pycodestyle": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"pyflakes": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"pylint": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"mccabe": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"pylsp_rope": {
|
|
||||||
"rename": false
|
|
||||||
},
|
|
||||||
"rope_rename": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"rope_autoimport": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"rope_completion": {
|
|
||||||
"enabled": false,
|
|
||||||
"eager": false
|
|
||||||
},
|
|
||||||
"jedi_rename": {
|
|
||||||
"enabled": true
|
|
||||||
},
|
|
||||||
"jedi_completion": {
|
|
||||||
"enabled": true,
|
|
||||||
"include_class_objects": true,
|
|
||||||
"include_function_objects": true,
|
|
||||||
"fuzzy": false
|
|
||||||
},
|
|
||||||
"jedi": {
|
|
||||||
"root_dir": "file://{workspace.folder}",
|
|
||||||
"extra_paths": [
|
|
||||||
"{user.home}/Portable_Apps/py-venvs/pylsp-venv/venv/lib/python3.10/site-packages"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"python - jedi-language-server": {
|
|
||||||
"hidden": true,
|
|
||||||
"info": "https://pypi.org/project/jedi-language-server/",
|
|
||||||
"command": "jedi-language-server",
|
|
||||||
"alt-command": "lsp-ws-proxy --listen 3030 -- jedi-language-server",
|
|
||||||
"socket": "ws://127.0.0.1:9999/python",
|
|
||||||
"socket-two": "ws://127.0.0.1:9999/?name=jedi-language-server",
|
|
||||||
"initialization-options": {
|
|
||||||
"jediSettings": {
|
|
||||||
"autoImportModules": [],
|
|
||||||
"caseInsensitiveCompletion": true,
|
|
||||||
"debug": false
|
|
||||||
},
|
|
||||||
"completion": {
|
|
||||||
"disableSnippets": false,
|
|
||||||
"resolveEagerly": false,
|
|
||||||
"ignorePatterns": []
|
|
||||||
},
|
|
||||||
"markupKindPreferred": "markdown",
|
|
||||||
"workspace": {
|
|
||||||
"extraPaths": [
|
|
||||||
"{user.home}/Portable_Apps/py-venvs/pylsp-venv/venv/lib/python3.10/site-packages"
|
|
||||||
],
|
|
||||||
"environmentPath": "{user.home}/Portable_Apps/py-venvs/gtk-apps-venv/venv/bin/python",
|
|
||||||
"symbols": {
|
|
||||||
"ignoreFolders": [
|
|
||||||
".nox",
|
|
||||||
".tox",
|
|
||||||
".venv",
|
|
||||||
"__pycache__",
|
|
||||||
"venv"
|
|
||||||
],
|
|
||||||
"maxSymbols": 20
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"cpp": {
|
|
||||||
"info": "https://clangd.llvm.org/",
|
|
||||||
"command": "lsp-ws-proxy -- clangd",
|
|
||||||
"alt-command": "clangd",
|
|
||||||
"socket": "ws://127.0.0.1:9999/cpp",
|
|
||||||
"socket-two": "ws://127.0.0.1:9999/?name=clangd",
|
|
||||||
"initialization-options": {}
|
|
||||||
},
|
|
||||||
"c": {
|
|
||||||
"hidden": true,
|
|
||||||
"info": "https://clangd.llvm.org/",
|
|
||||||
"command": "lsp-ws-proxy -- clangd",
|
|
||||||
"alt-command": "clangd",
|
|
||||||
"socket": "ws://127.0.0.1:9999/c",
|
|
||||||
"socket-two": "ws://127.0.0.1:9999/?name=clangd",
|
|
||||||
"initialization-options": {}
|
|
||||||
},
|
|
||||||
"go": {
|
|
||||||
"info": "https://pkg.go.dev/golang.org/x/tools/gopls#section-readme",
|
|
||||||
"command": "lsp-ws-proxy -- gopls",
|
|
||||||
"alt-command": "gopls",
|
|
||||||
"socket": "ws://127.0.0.1:9999/go",
|
|
||||||
"socket-two": "ws://127.0.0.1:9999/?name=gopls",
|
|
||||||
"initialization-options": {}
|
|
||||||
},
|
|
||||||
"typescript": {
|
|
||||||
"info": "https://github.com/typescript-language-server/typescript-language-server",
|
|
||||||
"command": "lsp-ws-proxy -- typescript-language-server",
|
|
||||||
"alt-command": "typescript-language-server --stdio",
|
|
||||||
"socket": "ws://127.0.0.1:9999/typescript",
|
|
||||||
"socket-two": "ws://127.0.0.1:9999/?name=ts",
|
|
||||||
"initialization-options": {}
|
|
||||||
},
|
|
||||||
"sh": {
|
|
||||||
"info": "",
|
|
||||||
"command": "",
|
|
||||||
"alt-command": "",
|
|
||||||
"socket": "ws://127.0.0.1:9999/bash",
|
|
||||||
"socket-two": "ws://127.0.0.1:9999/?name=shell",
|
|
||||||
"initialization-options": {}
|
|
||||||
},
|
|
||||||
"lua": {
|
|
||||||
"info": "https://github.com/LuaLS/lua-language-server",
|
|
||||||
"command": "lsp-ws-proxy -- lua-language-server",
|
|
||||||
"alt-command": "lua-language-server",
|
|
||||||
"socket": "ws://127.0.0.1:9999/lua",
|
|
||||||
"socket-two": "ws://127.0.0.1:9999/?name=lua",
|
|
||||||
"initialization-options": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
"""
|
|
||||||
Libs Code DTO(s) Events Package
|
|
||||||
"""
|
|
||||||
|
|
||||||
from .lsp_event import LspEvent
|
|
||||||
|
|
||||||
from .register_lsp_client_event import RegisterLspClientEvent
|
|
||||||
from .unregister_lsp_client_event import UnregisterLspClientEvent
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from libs.dto.code.events import CodeEvent
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LspEvent(CodeEvent):
|
|
||||||
...
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from ....response_handlers.base_handler import BaseHandler
|
|
||||||
|
|
||||||
from .lsp_event import LspEvent
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class RegisterLspClientEvent(LspEvent):
|
|
||||||
lang_id: str = ""
|
|
||||||
lang_config: str = "{}"
|
|
||||||
handler: BaseHandler = None
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from .lsp_event import LspEvent
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class UnregisterLspClientEvent(LspEvent):
|
|
||||||
lang_id: str = ""
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
import json
|
|
||||||
import enum
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from .lsp_structs import TextDocumentItem
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MessageEncoder(json.JSONEncoder):
|
|
||||||
"""
|
|
||||||
Encodes an object in JSON
|
|
||||||
"""
|
|
||||||
|
|
||||||
def default(self, o): # pylint: disable=E0202
|
|
||||||
return o.__dict__
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ClientRequest(object):
|
|
||||||
def __init__(self, id: int, method: str, params: dict):
|
|
||||||
"""
|
|
||||||
Constructs a new Client Request instance.
|
|
||||||
|
|
||||||
:param int id: Message id to track instance.
|
|
||||||
:param str method: The type of lsp request being made.
|
|
||||||
:param dict params: The arguments of the given method.
|
|
||||||
"""
|
|
||||||
self.jsonrpc = "2.0"
|
|
||||||
self.id = id
|
|
||||||
self.method = method
|
|
||||||
self.params = params
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ClientNotification(object):
|
|
||||||
def __init__(self, method: str, params: dict):
|
|
||||||
"""
|
|
||||||
Constructs a new Client Notification instance.
|
|
||||||
|
|
||||||
:param str method: The type of lsp notification being made.
|
|
||||||
:param dict params: The arguments of the given method.
|
|
||||||
"""
|
|
||||||
self.jsonrpc = "2.0"
|
|
||||||
self.method = method
|
|
||||||
self.params = params
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LSPResponseRequest(object):
|
|
||||||
"""
|
|
||||||
Constructs a new LSP Response Request instance.
|
|
||||||
|
|
||||||
:param id result: The id of the given message.
|
|
||||||
:param dict result: The arguments of the given method.
|
|
||||||
"""
|
|
||||||
jsonrpc: str
|
|
||||||
id: int
|
|
||||||
result: dict
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LSPResponseNotification(object):
|
|
||||||
"""
|
|
||||||
Constructs a new LSP Response Notification instance.
|
|
||||||
|
|
||||||
:param str method: The type of lsp notification being made.
|
|
||||||
:params dict result: The arguments of the given method.
|
|
||||||
"""
|
|
||||||
jsonrpc: str
|
|
||||||
method: str
|
|
||||||
params: dict
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LSPIDResponseNotification(object):
|
|
||||||
"""
|
|
||||||
Constructs a new LSP Response Notification instance.
|
|
||||||
|
|
||||||
:param str method: The type of lsp notification being made.
|
|
||||||
:params dict result: The arguments of the given method.
|
|
||||||
"""
|
|
||||||
jsonrpc: str
|
|
||||||
id: int
|
|
||||||
method: str
|
|
||||||
params: dict
|
|
||||||
|
|
||||||
|
|
||||||
class MessageTypes(ClientRequest, ClientNotification, LSPResponseRequest, LSPResponseNotification, LSPIDResponseNotification):
|
|
||||||
...
|
|
||||||
|
|
||||||
class ClientMessageTypes(ClientRequest, ClientNotification):
|
|
||||||
...
|
|
||||||
|
|
||||||
class LSPResponseTypes(LSPResponseRequest, LSPResponseNotification):
|
|
||||||
...
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
from .lsp_message_structs import MessageEncoder
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
LEN_HEADER = "Content-Length: "
|
|
||||||
TYPE_HEADER = "Content-Type: "
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_message_str(data: dict) -> str:
|
|
||||||
return json.dumps(data, separators = (',', ':'), indent = 4, cls = MessageEncoder)
|
|
||||||
|
|
||||||
def get_message_obj(data: str):
|
|
||||||
return json.loads(data)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Request type formatting
|
|
||||||
# https://github.com/microsoft/multilspy/blob/main/src/multilspy/language_server.py#L417
|
|
||||||
content_part = {
|
|
||||||
"method": "textDocument/definition",
|
|
||||||
"params": {
|
|
||||||
"textDocument": {
|
|
||||||
"uri": "file://",
|
|
||||||
"languageId": "python",
|
|
||||||
"version": 1,
|
|
||||||
"text": ""
|
|
||||||
},
|
|
||||||
"position": {
|
|
||||||
"line": 5,
|
|
||||||
"character": 12,
|
|
||||||
"offset": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
didopen_notification = {
|
|
||||||
"method": "textDocument/didOpen",
|
|
||||||
"params": {
|
|
||||||
"textDocument": {
|
|
||||||
"uri": "file://",
|
|
||||||
"languageId": "python",
|
|
||||||
"version": 1,
|
|
||||||
"text": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
didsave_notification = {
|
|
||||||
"method": "textDocument/didSave",
|
|
||||||
"params": {
|
|
||||||
"textDocument": {
|
|
||||||
"uri": "file://"
|
|
||||||
},
|
|
||||||
"text": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
didclose_notification = {
|
|
||||||
"method": "textDocument/didClose",
|
|
||||||
"params": {
|
|
||||||
"textDocument": {
|
|
||||||
"uri": "file://"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
didchange_notification = {
|
|
||||||
"method": "textDocument/didChange",
|
|
||||||
"params": {
|
|
||||||
"textDocument": {
|
|
||||||
"uri": "file://",
|
|
||||||
"languageId": "python",
|
|
||||||
"version": 1,
|
|
||||||
},
|
|
||||||
"contentChanges": [
|
|
||||||
{
|
|
||||||
"text": ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
didchange_notification_range = {
|
|
||||||
"method": "textDocument/didChange",
|
|
||||||
"params": {
|
|
||||||
"textDocument": {
|
|
||||||
"uri": "file://",
|
|
||||||
"languageId": "python",
|
|
||||||
"version": 1,
|
|
||||||
"text": ""
|
|
||||||
},
|
|
||||||
"contentChanges": [
|
|
||||||
{
|
|
||||||
"range": {
|
|
||||||
"start": {
|
|
||||||
"line": 1,
|
|
||||||
"character": 1,
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"line": 1,
|
|
||||||
"character": 1,
|
|
||||||
},
|
|
||||||
"rangeLength": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# CompletionTriggerKind = 1 | 2 | 3;
|
|
||||||
# export const Invoked: 1 = 1;
|
|
||||||
# export const TriggerCharacter: 2 = 2;
|
|
||||||
# export const TriggerForIncompleteCompletions: 3 = 3;
|
|
||||||
completion_request = {
|
|
||||||
"method": "textDocument/completion",
|
|
||||||
"params": {
|
|
||||||
"textDocument": {
|
|
||||||
"uri": "file://",
|
|
||||||
"languageId": "python",
|
|
||||||
"version": 1,
|
|
||||||
"text": ""
|
|
||||||
},
|
|
||||||
"position": {
|
|
||||||
"line": 5,
|
|
||||||
"character": 12,
|
|
||||||
"offset": 0
|
|
||||||
},
|
|
||||||
"contet": {
|
|
||||||
"triggerKind": 3,
|
|
||||||
"triggerCharacter": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
definition_request = {
|
|
||||||
"method": "textDocument/definition",
|
|
||||||
"params": {
|
|
||||||
"textDocument": {
|
|
||||||
"uri": "file://",
|
|
||||||
"languageId": "python",
|
|
||||||
"version": 1,
|
|
||||||
"text": ""
|
|
||||||
},
|
|
||||||
"position": {
|
|
||||||
"line": 5,
|
|
||||||
"character": 12,
|
|
||||||
"offset": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
references_request = {
|
|
||||||
"method": "textDocument/references",
|
|
||||||
"params": {
|
|
||||||
"context": {
|
|
||||||
"includeDeclaration": False
|
|
||||||
},
|
|
||||||
"textDocument": {
|
|
||||||
"uri": "file://",
|
|
||||||
"languageId": "python",
|
|
||||||
"version": 1,
|
|
||||||
"text": ""
|
|
||||||
},
|
|
||||||
"position": {
|
|
||||||
"line": 30,
|
|
||||||
"character": 13,
|
|
||||||
"offset": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
symbols_request = {
|
|
||||||
"method": "textDocument/documentSymbol",
|
|
||||||
"params": {
|
|
||||||
"textDocument": {
|
|
||||||
"uri": "file://",
|
|
||||||
"languageId": "python",
|
|
||||||
"version": 1,
|
|
||||||
"text": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,571 +0,0 @@
|
|||||||
# Python imports
|
|
||||||
import enum
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
# Application imports
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def to_type(o, new_type):
|
|
||||||
'''
|
|
||||||
Helper funciton that receives an object or a dict and convert it to a new
|
|
||||||
given type.
|
|
||||||
|
|
||||||
:param object|dict o: The object to convert
|
|
||||||
:param Type new_type: The type to convert to.
|
|
||||||
'''
|
|
||||||
|
|
||||||
return o if new_type == type(o) else new_type(**o)
|
|
||||||
|
|
||||||
|
|
||||||
class Position(object):
|
|
||||||
def __init__(self, line, character):
|
|
||||||
"""
|
|
||||||
Constructs a new Position instance.
|
|
||||||
|
|
||||||
:param int line: Line position in a document (zero-based).
|
|
||||||
:param int character: Character offset on a line in a document
|
|
||||||
(zero-based).
|
|
||||||
"""
|
|
||||||
self.line = line
|
|
||||||
self.character = character
|
|
||||||
|
|
||||||
|
|
||||||
class Range(object):
|
|
||||||
def __init__(self, start, end):
|
|
||||||
"""
|
|
||||||
Constructs a new Range instance.
|
|
||||||
|
|
||||||
:param Position start: The range's start position.
|
|
||||||
:param Position end: The range's end position.
|
|
||||||
"""
|
|
||||||
self.start = to_type(start, Position)
|
|
||||||
self.end = to_type(end, Position)
|
|
||||||
|
|
||||||
|
|
||||||
class Location(object):
|
|
||||||
"""
|
|
||||||
Represents a location inside a resource, such as a line inside a text file.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, uri, range):
|
|
||||||
"""
|
|
||||||
Constructs a new Location instance.
|
|
||||||
|
|
||||||
:param str uri: Resource file.
|
|
||||||
:param Range range: The range inside the file
|
|
||||||
"""
|
|
||||||
self.uri = uri
|
|
||||||
self.range = to_type(range, Range)
|
|
||||||
|
|
||||||
|
|
||||||
class LocationLink(object):
|
|
||||||
"""
|
|
||||||
Represents a link between a source and a target location.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, originSelectionRange, targetUri, targetRange, targetSelectionRange):
|
|
||||||
"""
|
|
||||||
Constructs a new LocationLink instance.
|
|
||||||
|
|
||||||
:param Range originSelectionRange: Span of the origin of this link.
|
|
||||||
Used as the underlined span for mouse interaction. Defaults to the word range at the mouse position.
|
|
||||||
:param str targetUri: The target resource identifier of this link.
|
|
||||||
:param Range targetRange: The full target range of this link. If the target for example is a symbol then target
|
|
||||||
range is the range enclosing this symbol not including leading/trailing whitespace but everything else
|
|
||||||
like comments. This information is typically used to highlight the range in the editor.
|
|
||||||
:param Range targetSelectionRange: The range that should be selected and revealed when this link is being followed,
|
|
||||||
e.g the name of a function. Must be contained by the the `targetRange`. See also `DocumentSymbol#range`
|
|
||||||
"""
|
|
||||||
self.originSelectionRange = to_type(originSelectionRange, Range)
|
|
||||||
self.targetUri = targetUri
|
|
||||||
self.targetRange = to_type(targetRange, Range)
|
|
||||||
self.targetSelectionRange = to_type(targetSelectionRange, Range)
|
|
||||||
|
|
||||||
|
|
||||||
class Diagnostic(object):
|
|
||||||
def __init__(self, range, severity, code, source, message, relatedInformation):
|
|
||||||
"""
|
|
||||||
Constructs a new Diagnostic instance.
|
|
||||||
:param Range range: The range at which the message applies.Resource file.
|
|
||||||
:param int severity: The diagnostic's severity. Can be omitted. If omitted it is up to the
|
|
||||||
client to interpret diagnostics as error, warning, info or hint.
|
|
||||||
:param str code: The diagnostic's code, which might appear in the user interface.
|
|
||||||
:param str source: A human-readable string describing the source of this
|
|
||||||
diagnostic, e.g. 'typescript' or 'super lint'.
|
|
||||||
:param str message: The diagnostic's message.
|
|
||||||
:param list relatedInformation: An array of related diagnostic information, e.g. when symbol-names within
|
|
||||||
a scope collide all definitions can be marked via this property.
|
|
||||||
"""
|
|
||||||
self.range = range
|
|
||||||
self.severity = severity
|
|
||||||
self.code = code
|
|
||||||
self.source = source
|
|
||||||
self.message = message
|
|
||||||
self.relatedInformation = relatedInformation
|
|
||||||
|
|
||||||
|
|
||||||
class DiagnosticSeverity(object):
|
|
||||||
Error = 1
|
|
||||||
Warning = 2 # TODO: warning is known in python
|
|
||||||
Information = 3
|
|
||||||
Hint = 4
|
|
||||||
|
|
||||||
|
|
||||||
class DiagnosticRelatedInformation(object):
|
|
||||||
def __init__(self, location, message):
|
|
||||||
"""
|
|
||||||
Constructs a new Diagnostic instance.
|
|
||||||
:param Location location: The location of this related diagnostic information.
|
|
||||||
:param str message: The message of this related diagnostic information.
|
|
||||||
"""
|
|
||||||
self.location = location
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
|
|
||||||
class Command(object):
|
|
||||||
def __init__(self, title, command, arguments):
|
|
||||||
"""
|
|
||||||
Constructs a new Diagnostic instance.
|
|
||||||
:param str title: Title of the command, like `save`.
|
|
||||||
:param str command: The identifier of the actual command handler.
|
|
||||||
:param list argusments: Arguments that the command handler should be invoked with.
|
|
||||||
"""
|
|
||||||
self.title = title
|
|
||||||
self.command = command
|
|
||||||
self.arguments = arguments
|
|
||||||
|
|
||||||
|
|
||||||
class TextDocumentItem(object):
|
|
||||||
"""
|
|
||||||
An item to transfer a text document from the client to the server.
|
|
||||||
"""
|
|
||||||
def __init__(self, uri, languageId, version, text):
|
|
||||||
"""
|
|
||||||
Constructs a new Diagnostic instance.
|
|
||||||
|
|
||||||
:param DocumentUri uri: uri file path.
|
|
||||||
:param str languageId: The identifier of the actual command handler.
|
|
||||||
:param int version: Arguments that the command handler should be invoked with.
|
|
||||||
:param str text: Arguments that the command handler should be invoked with.
|
|
||||||
"""
|
|
||||||
self.uri = uri
|
|
||||||
self.languageId = languageId
|
|
||||||
self.version = version
|
|
||||||
self.text = text
|
|
||||||
|
|
||||||
|
|
||||||
class TextDocumentIdentifier(object):
|
|
||||||
"""
|
|
||||||
Text documents are identified using a URI. On the protocol level, URIs are passed as strings.
|
|
||||||
"""
|
|
||||||
def __init__(self, uri):
|
|
||||||
"""
|
|
||||||
Constructs a new TextDocumentIdentifier instance.
|
|
||||||
|
|
||||||
:param DocumentUri uri: The text document's URI.
|
|
||||||
"""
|
|
||||||
self.uri = uri
|
|
||||||
|
|
||||||
|
|
||||||
class VersionedTextDocumentIdentifier(TextDocumentIdentifier):
|
|
||||||
"""
|
|
||||||
An identifier to denote a specific version of a text document.
|
|
||||||
"""
|
|
||||||
def __init__(self, uri, version):
|
|
||||||
"""
|
|
||||||
Constructs a new TextDocumentIdentifier instance.
|
|
||||||
|
|
||||||
:param DocumentUri uri: The text document's URI.
|
|
||||||
:param int version: The version number of this document. If a versioned
|
|
||||||
text document identifier is sent from the server to the client and
|
|
||||||
the file is not open in the editor (the server has not received an
|
|
||||||
open notification before) the server can send `null` to indicate
|
|
||||||
that the version is known and the content on disk is the truth (as
|
|
||||||
speced with document content ownership).
|
|
||||||
The version number of a document will increase after each change, including
|
|
||||||
undo/redo. The number doesn't need to be consecutive.
|
|
||||||
"""
|
|
||||||
super(VersionedTextDocumentIdentifier, self).__init__(uri)
|
|
||||||
self.version = version
|
|
||||||
|
|
||||||
|
|
||||||
class TextDocumentContentChangeEvent(object):
|
|
||||||
"""
|
|
||||||
An event describing a change to a text document. If range and rangeLength are omitted
|
|
||||||
the new text is considered to be the full content of the document.
|
|
||||||
"""
|
|
||||||
def __init__(self, range, rangeLength, text):
|
|
||||||
"""
|
|
||||||
Constructs a new TextDocumentContentChangeEvent instance.
|
|
||||||
|
|
||||||
:param Range range: The range of the document that changed.
|
|
||||||
:param int rangeLength: The length of the range that got replaced.
|
|
||||||
:param str text: The new text of the range/document.
|
|
||||||
"""
|
|
||||||
self.range = range
|
|
||||||
self.rangeLength = rangeLength
|
|
||||||
self.text = text
|
|
||||||
|
|
||||||
|
|
||||||
class TextDocumentPositionParams(object):
|
|
||||||
"""
|
|
||||||
A parameter literal used in requests to pass a text document and a position inside that document.
|
|
||||||
"""
|
|
||||||
def __init__(self, textDocument, position):
|
|
||||||
"""
|
|
||||||
Constructs a new TextDocumentPositionParams instance.
|
|
||||||
|
|
||||||
:param TextDocumentIdentifier textDocument: The text document.
|
|
||||||
:param Position position: The position inside the text document.
|
|
||||||
"""
|
|
||||||
self.textDocument = textDocument
|
|
||||||
self.position = position
|
|
||||||
|
|
||||||
|
|
||||||
class LANGUAGE_IDENTIFIER(object):
|
|
||||||
BAT = "bat"
|
|
||||||
BIBTEX = "bibtex"
|
|
||||||
CLOJURE = "clojure"
|
|
||||||
COFFESCRIPT = "coffeescript"
|
|
||||||
C = "c"
|
|
||||||
CPP = "cpp"
|
|
||||||
CSHARP = "csharp"
|
|
||||||
CSS = "css"
|
|
||||||
DIFF = "diff"
|
|
||||||
DOCKERFILE = "dockerfile"
|
|
||||||
FSHARP = "fsharp"
|
|
||||||
GIT_COMMIT = "git-commit"
|
|
||||||
GIT_REBASE = "git-rebase"
|
|
||||||
GO = "go"
|
|
||||||
GROOVY = "groovy"
|
|
||||||
HANDLEBARS = "handlebars"
|
|
||||||
HTML = "html"
|
|
||||||
INI = "ini"
|
|
||||||
JAVA = "java"
|
|
||||||
JAVASCRIPT = "javascript"
|
|
||||||
JSON = "json"
|
|
||||||
LATEX = "latex"
|
|
||||||
LESS = "less"
|
|
||||||
LUA = "lua"
|
|
||||||
MAKEFILE = "makefile"
|
|
||||||
MARKDOWN = "markdown"
|
|
||||||
OBJECTIVE_C = "objective-c"
|
|
||||||
OBJECTIVE_CPP = "objective-cpp"
|
|
||||||
Perl = "perl"
|
|
||||||
PHP = "php"
|
|
||||||
POWERSHELL = "powershell"
|
|
||||||
PUG = "jade"
|
|
||||||
PYTHON = "python"
|
|
||||||
R = "r"
|
|
||||||
RAZOR = "razor"
|
|
||||||
RUBY = "ruby"
|
|
||||||
RUST = "rust"
|
|
||||||
SASS = "sass"
|
|
||||||
SCSS = "scss"
|
|
||||||
ShaderLab = "shaderlab"
|
|
||||||
SHELL_SCRIPT = "shellscript"
|
|
||||||
SQL = "sql"
|
|
||||||
SWIFT = "swift"
|
|
||||||
TYPE_SCRIPT = "typescript"
|
|
||||||
TEX = "tex"
|
|
||||||
VB = "vb"
|
|
||||||
XML = "xml"
|
|
||||||
XSL = "xsl"
|
|
||||||
YAML = "yaml"
|
|
||||||
|
|
||||||
|
|
||||||
class SymbolKind(enum.Enum):
|
|
||||||
File = 1
|
|
||||||
Module = 2
|
|
||||||
Namespace = 3
|
|
||||||
Package = 4
|
|
||||||
Class = 5
|
|
||||||
Method = 6
|
|
||||||
Property = 7
|
|
||||||
Field = 8
|
|
||||||
Constructor = 9
|
|
||||||
Enum = 10
|
|
||||||
Interface = 11
|
|
||||||
Function = 12
|
|
||||||
Variable = 13
|
|
||||||
Constant = 14
|
|
||||||
String = 15
|
|
||||||
Number = 16
|
|
||||||
Boolean = 17
|
|
||||||
Array = 18
|
|
||||||
Object = 19
|
|
||||||
Key = 20
|
|
||||||
Null = 21
|
|
||||||
EnumMember = 22
|
|
||||||
Struct = 23
|
|
||||||
Event = 24
|
|
||||||
Operator = 25
|
|
||||||
TypeParameter = 26
|
|
||||||
|
|
||||||
|
|
||||||
class SymbolInformation(object):
|
|
||||||
"""
|
|
||||||
Represents information about programming constructs like variables, classes, interfaces etc.
|
|
||||||
"""
|
|
||||||
def __init__(self, name, kind, location, containerName = None, deprecated = False):
|
|
||||||
"""
|
|
||||||
Constructs a new SymbolInformation instance.
|
|
||||||
|
|
||||||
:param str name: The name of this symbol.
|
|
||||||
:param int kind: The kind of this symbol.
|
|
||||||
:param bool Location: The location of this symbol. The location's range is used by a tool
|
|
||||||
to reveal the location in the editor. If the symbol is selected in the
|
|
||||||
tool the range's start information is used to position the cursor. So
|
|
||||||
the range usually spans more then the actual symbol's name and does
|
|
||||||
normally include things like visibility modifiers.
|
|
||||||
|
|
||||||
The range doesn't have to denote a node range in the sense of a abstract
|
|
||||||
syntax tree. It can therefore not be used to re-construct a hierarchy of
|
|
||||||
the symbols.
|
|
||||||
:param str containerName: The name of the symbol containing this symbol. This information is for
|
|
||||||
user interface purposes (e.g. to render a qualifier in the user interface
|
|
||||||
if necessary). It can't be used to re-infer a hierarchy for the document
|
|
||||||
symbols.
|
|
||||||
:param bool deprecated: Indicates if this symbol is deprecated.
|
|
||||||
"""
|
|
||||||
self.name = name
|
|
||||||
self.kind = SymbolKind(kind)
|
|
||||||
self.deprecated = deprecated
|
|
||||||
self.location = to_type(location, Location)
|
|
||||||
self.containerName = containerName
|
|
||||||
|
|
||||||
|
|
||||||
class ParameterInformation(object):
|
|
||||||
"""
|
|
||||||
Represents a parameter of a callable-signature. A parameter can
|
|
||||||
have a label and a doc-comment.
|
|
||||||
"""
|
|
||||||
def __init__(self, label, documentation = ""):
|
|
||||||
"""
|
|
||||||
Constructs a new ParameterInformation instance.
|
|
||||||
|
|
||||||
:param str label: The label of this parameter. Will be shown in the UI.
|
|
||||||
:param str documentation: The human-readable doc-comment of this parameter. Will be shown in the UI but can be omitted.
|
|
||||||
"""
|
|
||||||
self.label = label
|
|
||||||
self.documentation = documentation
|
|
||||||
|
|
||||||
|
|
||||||
class SignatureInformation(object):
|
|
||||||
"""
|
|
||||||
Represents the signature of something callable. A signature
|
|
||||||
can have a label, like a function-name, a doc-comment, and
|
|
||||||
a set of parameters.
|
|
||||||
"""
|
|
||||||
def __init__(self, label, documentation = "", parameters = []):
|
|
||||||
"""
|
|
||||||
Constructs a new SignatureInformation instance.
|
|
||||||
|
|
||||||
:param str label: The label of this signature. Will be shown in the UI.
|
|
||||||
:param str documentation: The human-readable doc-comment of this signature. Will be shown in the UI but can be omitted.
|
|
||||||
:param ParameterInformation[] parameters: The parameters of this signature.
|
|
||||||
"""
|
|
||||||
self.label = label
|
|
||||||
self.documentation = documentation
|
|
||||||
self.parameters = [to_type(parameter, ParameterInformation) for parameter in parameters]
|
|
||||||
|
|
||||||
|
|
||||||
class SignatureHelp(object):
|
|
||||||
"""
|
|
||||||
Signature help represents the signature of something
|
|
||||||
callable. There can be multiple signature but only one
|
|
||||||
active and only one active parameter.
|
|
||||||
"""
|
|
||||||
def __init__(self, signatures, activeSignature = 0, activeParameter = 0):
|
|
||||||
"""
|
|
||||||
Constructs a new SignatureHelp instance.
|
|
||||||
|
|
||||||
:param SignatureInformation[] signatures: One or more signatures.
|
|
||||||
:param int activeSignature:
|
|
||||||
:param int activeParameter:
|
|
||||||
"""
|
|
||||||
self.signatures = [to_type(signature, SignatureInformation) for signature in signatures]
|
|
||||||
self.activeSignature = activeSignature
|
|
||||||
self.activeParameter = activeParameter
|
|
||||||
|
|
||||||
|
|
||||||
class CompletionTriggerKind(object):
|
|
||||||
Invoked = 1
|
|
||||||
TriggerCharacter = 2
|
|
||||||
TriggerForIncompleteCompletions = 3
|
|
||||||
|
|
||||||
|
|
||||||
class CompletionContext(object):
|
|
||||||
"""
|
|
||||||
Contains additional information about the context in which a completion request is triggered.
|
|
||||||
"""
|
|
||||||
def __init__(self, triggerKind, triggerCharacter = None):
|
|
||||||
"""
|
|
||||||
Constructs a new CompletionContext instance.
|
|
||||||
|
|
||||||
:param CompletionTriggerKind triggerKind: How the completion was triggered.
|
|
||||||
:param str triggerCharacter: The trigger character (a single character) that has trigger code complete.
|
|
||||||
Is undefined if `triggerKind !== CompletionTriggerKind.TriggerCharacter`
|
|
||||||
"""
|
|
||||||
self.triggerKind = triggerKind
|
|
||||||
if triggerCharacter:
|
|
||||||
self.triggerCharacter = triggerCharacter
|
|
||||||
|
|
||||||
|
|
||||||
class TextEdit(object):
|
|
||||||
"""
|
|
||||||
A textual edit applicable to a text document.
|
|
||||||
"""
|
|
||||||
def __init__(self, range, newText):
|
|
||||||
"""
|
|
||||||
:param Range range: The range of the text document to be manipulated. To insert
|
|
||||||
text into a document create a range where start === end.
|
|
||||||
:param str newText: The string to be inserted. For delete operations use an empty string.
|
|
||||||
"""
|
|
||||||
self.range = range
|
|
||||||
self.newText = newText
|
|
||||||
|
|
||||||
|
|
||||||
class InsertTextFormat(object):
|
|
||||||
PlainText = 1
|
|
||||||
Snippet = 2
|
|
||||||
|
|
||||||
|
|
||||||
class CompletionItem(object):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
def __init__(self, label, \
|
|
||||||
kind = None, \
|
|
||||||
detail = None, \
|
|
||||||
documentation = None, \
|
|
||||||
deprecated = None, \
|
|
||||||
preselect = None, \
|
|
||||||
sortText = None, \
|
|
||||||
filterText = None, \
|
|
||||||
insertText = None, \
|
|
||||||
insertTextFormat = None, \
|
|
||||||
textEdit = None, \
|
|
||||||
additionalTextEdits = None, \
|
|
||||||
commitCharacters = None, \
|
|
||||||
command = None, \
|
|
||||||
data = None, \
|
|
||||||
score = 0.0
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
:param str label: The label of this completion item. By default also the text that is inserted when selecting
|
|
||||||
this completion.
|
|
||||||
:param int kind: The kind of this completion item. Based of the kind an icon is chosen by the editor.
|
|
||||||
:param str detail: A human-readable string with additional information about this item, like type or symbol information.
|
|
||||||
:param tr ocumentation: A human-readable string that represents a doc-comment.
|
|
||||||
:param bool deprecated: Indicates if this item is deprecated.
|
|
||||||
:param bool preselect: Select this item when showing. Note: that only one completion item can be selected and that the
|
|
||||||
tool / client decides which item that is. The rule is that the first item of those that match best is selected.
|
|
||||||
:param str sortText: A string that should be used when comparing this item with other items. When `falsy` the label is used.
|
|
||||||
:param str filterText: A string that should be used when filtering a set of completion items. When `falsy` the label is used.
|
|
||||||
:param str insertText: A string that should be inserted into a document when selecting this completion. When `falsy` the label is used.
|
|
||||||
The `insertText` is subject to interpretation by the client side. Some tools might not take the string literally. For example
|
|
||||||
VS Code when code complete is requested in this example `con<cursor position>` and a completion item with an `insertText` of `console` is provided it
|
|
||||||
will only insert `sole`. Therefore it is recommended to use `textEdit` instead since it avoids additional client side interpretation.
|
|
||||||
@deprecated Use textEdit instead.
|
|
||||||
:param InsertTextFormat insertTextFormat: The format of the insert text. The format applies to both the `insertText` property
|
|
||||||
and the `newText` property of a provided `textEdit`.
|
|
||||||
:param TextEdit textEdit: An edit which is applied to a document when selecting this completion. When an edit is provided the value of `insertText` is ignored.
|
|
||||||
Note:* The range of the edit must be a single line range and it must contain the position at which completion
|
|
||||||
has been requested.
|
|
||||||
:param TextEdit additionalTextEdits: An optional array of additional text edits that are applied when selecting this completion.
|
|
||||||
Edits must not overlap (including the same insert position) with the main edit nor with themselves.
|
|
||||||
Additional text edits should be used to change text unrelated to the current cursor position
|
|
||||||
(for example adding an import statement at the top of the file if the completion item will
|
|
||||||
insert an unqualified type).
|
|
||||||
:param str commitCharacters: An optional set of characters that when pressed while this completion is active will accept it first and
|
|
||||||
then type that character. *Note* that all commit characters should have `length=1` and that superfluous
|
|
||||||
characters will be ignored.
|
|
||||||
:param Command command: An optional command that is executed *after* inserting this completion. Note: that
|
|
||||||
additional modifications to the current document should be described with the additionalTextEdits-property.
|
|
||||||
:param data: An data entry field that is preserved on a completion item between a completion and a completion resolve request.
|
|
||||||
:param float score: Score of the code completion item.
|
|
||||||
"""
|
|
||||||
self.label = label
|
|
||||||
self.kind = kind
|
|
||||||
self.detail = detail
|
|
||||||
self.documentation = documentation
|
|
||||||
self.deprecated = deprecated
|
|
||||||
self.preselect = preselect
|
|
||||||
self.sortText = sortText
|
|
||||||
self.filterText = filterText
|
|
||||||
self.insertText = insertText
|
|
||||||
self.insertTextFormat = insertTextFormat
|
|
||||||
self.textEdit = textEdit
|
|
||||||
self.additionalTextEdits = additionalTextEdits
|
|
||||||
self.commitCharacters = commitCharacters
|
|
||||||
self.command = command
|
|
||||||
self.data = data
|
|
||||||
self.score = score
|
|
||||||
|
|
||||||
|
|
||||||
class CompletionItemKind(enum.Enum):
|
|
||||||
Text = 1
|
|
||||||
Method = 2
|
|
||||||
Function = 3
|
|
||||||
Constructor = 4
|
|
||||||
Field = 5
|
|
||||||
Variable = 6
|
|
||||||
Class = 7
|
|
||||||
Interface = 8
|
|
||||||
Module = 9
|
|
||||||
Property = 10
|
|
||||||
Unit = 11
|
|
||||||
Value = 12
|
|
||||||
Enum = 13
|
|
||||||
Keyword = 14
|
|
||||||
Snippet = 15
|
|
||||||
Color = 16
|
|
||||||
File = 17
|
|
||||||
Reference = 18
|
|
||||||
Folder = 19
|
|
||||||
EnumMember = 20
|
|
||||||
Constant = 21
|
|
||||||
Struct = 22
|
|
||||||
Event = 23
|
|
||||||
Operator = 24
|
|
||||||
TypeParameter = 25
|
|
||||||
|
|
||||||
|
|
||||||
class CompletionList(object):
|
|
||||||
"""
|
|
||||||
Represents a collection of [completion items](#CompletionItem) to be preselect in the editor.
|
|
||||||
"""
|
|
||||||
def __init__(self, isIncomplete, items):
|
|
||||||
"""
|
|
||||||
Constructs a new CompletionContext instance.
|
|
||||||
|
|
||||||
:param bool isIncomplete: This list it not complete. Further typing should result in recomputing this list.
|
|
||||||
:param CompletionItem items: The completion items.
|
|
||||||
"""
|
|
||||||
self.isIncomplete = isIncomplete
|
|
||||||
self.items = [to_type(i, CompletionItem) for i in items]
|
|
||||||
|
|
||||||
class ErrorCodes(enum.Enum):
|
|
||||||
# Defined by JSON RPC
|
|
||||||
ParseError = -32700
|
|
||||||
InvalidRequest = -32600
|
|
||||||
MethodNotFound = -32601
|
|
||||||
InvalidParams = -32602
|
|
||||||
InternalError = -32603
|
|
||||||
serverErrorStart = -32099
|
|
||||||
serverErrorEnd = -32000
|
|
||||||
ServerNotInitialized = -32002
|
|
||||||
UnknownErrorCode = -32001
|
|
||||||
|
|
||||||
# Defined by the protocol.
|
|
||||||
RequestCancelled = -32800
|
|
||||||
ContentModified = -32801
|
|
||||||
|
|
||||||
class ResponseError(Exception):
|
|
||||||
def __init__(self, code, message, data = None):
|
|
||||||
self.code = code
|
|
||||||
self.message = message
|
|
||||||
if data:
|
|
||||||
self.data = data
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
"""
|
|
||||||
Pligin Libs Module
|
|
||||||
"""
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
"""
|
|
||||||
__init__.py
|
|
||||||
websocket - WebSocket client library for Python
|
|
||||||
|
|
||||||
Copyright 2024 engn33r
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from ._abnf import *
|
|
||||||
from ._app import WebSocketApp as WebSocketApp, setReconnect as setReconnect
|
|
||||||
from ._core import *
|
|
||||||
from ._exceptions import *
|
|
||||||
from ._logging import *
|
|
||||||
from ._socket import *
|
|
||||||
|
|
||||||
__version__ = "1.8.0"
|
|
||||||
@@ -1,453 +0,0 @@
|
|||||||
import array
|
|
||||||
import os
|
|
||||||
import struct
|
|
||||||
import sys
|
|
||||||
from threading import Lock
|
|
||||||
from typing import Callable, Optional, Union
|
|
||||||
|
|
||||||
from ._exceptions import WebSocketPayloadException, WebSocketProtocolException
|
|
||||||
from ._utils import validate_utf8
|
|
||||||
|
|
||||||
"""
|
|
||||||
_abnf.py
|
|
||||||
websocket - WebSocket client library for Python
|
|
||||||
|
|
||||||
Copyright 2024 engn33r
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
# If wsaccel is available, use compiled routines to mask data.
|
|
||||||
# wsaccel only provides around a 10% speed boost compared
|
|
||||||
# to the websocket-client _mask() implementation.
|
|
||||||
# Note that wsaccel is unmaintained.
|
|
||||||
from wsaccel.xormask import XorMaskerSimple
|
|
||||||
|
|
||||||
def _mask(mask_value: array.array, data_value: array.array) -> bytes:
|
|
||||||
mask_result: bytes = XorMaskerSimple(mask_value).process(data_value)
|
|
||||||
return mask_result
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
# wsaccel is not available, use websocket-client _mask()
|
|
||||||
native_byteorder = sys.byteorder
|
|
||||||
|
|
||||||
def _mask(mask_value: array.array, data_value: array.array) -> bytes:
|
|
||||||
datalen = len(data_value)
|
|
||||||
int_data_value = int.from_bytes(data_value, native_byteorder)
|
|
||||||
int_mask_value = int.from_bytes(
|
|
||||||
mask_value * (datalen // 4) + mask_value[: datalen % 4], native_byteorder
|
|
||||||
)
|
|
||||||
return (int_data_value ^ int_mask_value).to_bytes(datalen, native_byteorder)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"ABNF",
|
|
||||||
"continuous_frame",
|
|
||||||
"frame_buffer",
|
|
||||||
"STATUS_NORMAL",
|
|
||||||
"STATUS_GOING_AWAY",
|
|
||||||
"STATUS_PROTOCOL_ERROR",
|
|
||||||
"STATUS_UNSUPPORTED_DATA_TYPE",
|
|
||||||
"STATUS_STATUS_NOT_AVAILABLE",
|
|
||||||
"STATUS_ABNORMAL_CLOSED",
|
|
||||||
"STATUS_INVALID_PAYLOAD",
|
|
||||||
"STATUS_POLICY_VIOLATION",
|
|
||||||
"STATUS_MESSAGE_TOO_BIG",
|
|
||||||
"STATUS_INVALID_EXTENSION",
|
|
||||||
"STATUS_UNEXPECTED_CONDITION",
|
|
||||||
"STATUS_BAD_GATEWAY",
|
|
||||||
"STATUS_TLS_HANDSHAKE_ERROR",
|
|
||||||
]
|
|
||||||
|
|
||||||
# closing frame status codes.
|
|
||||||
STATUS_NORMAL = 1000
|
|
||||||
STATUS_GOING_AWAY = 1001
|
|
||||||
STATUS_PROTOCOL_ERROR = 1002
|
|
||||||
STATUS_UNSUPPORTED_DATA_TYPE = 1003
|
|
||||||
STATUS_STATUS_NOT_AVAILABLE = 1005
|
|
||||||
STATUS_ABNORMAL_CLOSED = 1006
|
|
||||||
STATUS_INVALID_PAYLOAD = 1007
|
|
||||||
STATUS_POLICY_VIOLATION = 1008
|
|
||||||
STATUS_MESSAGE_TOO_BIG = 1009
|
|
||||||
STATUS_INVALID_EXTENSION = 1010
|
|
||||||
STATUS_UNEXPECTED_CONDITION = 1011
|
|
||||||
STATUS_SERVICE_RESTART = 1012
|
|
||||||
STATUS_TRY_AGAIN_LATER = 1013
|
|
||||||
STATUS_BAD_GATEWAY = 1014
|
|
||||||
STATUS_TLS_HANDSHAKE_ERROR = 1015
|
|
||||||
|
|
||||||
VALID_CLOSE_STATUS = (
|
|
||||||
STATUS_NORMAL,
|
|
||||||
STATUS_GOING_AWAY,
|
|
||||||
STATUS_PROTOCOL_ERROR,
|
|
||||||
STATUS_UNSUPPORTED_DATA_TYPE,
|
|
||||||
STATUS_INVALID_PAYLOAD,
|
|
||||||
STATUS_POLICY_VIOLATION,
|
|
||||||
STATUS_MESSAGE_TOO_BIG,
|
|
||||||
STATUS_INVALID_EXTENSION,
|
|
||||||
STATUS_UNEXPECTED_CONDITION,
|
|
||||||
STATUS_SERVICE_RESTART,
|
|
||||||
STATUS_TRY_AGAIN_LATER,
|
|
||||||
STATUS_BAD_GATEWAY,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ABNF:
|
|
||||||
"""
|
|
||||||
ABNF frame class.
|
|
||||||
See http://tools.ietf.org/html/rfc5234
|
|
||||||
and http://tools.ietf.org/html/rfc6455#section-5.2
|
|
||||||
"""
|
|
||||||
|
|
||||||
# operation code values.
|
|
||||||
OPCODE_CONT = 0x0
|
|
||||||
OPCODE_TEXT = 0x1
|
|
||||||
OPCODE_BINARY = 0x2
|
|
||||||
OPCODE_CLOSE = 0x8
|
|
||||||
OPCODE_PING = 0x9
|
|
||||||
OPCODE_PONG = 0xA
|
|
||||||
|
|
||||||
# available operation code value tuple
|
|
||||||
OPCODES = (
|
|
||||||
OPCODE_CONT,
|
|
||||||
OPCODE_TEXT,
|
|
||||||
OPCODE_BINARY,
|
|
||||||
OPCODE_CLOSE,
|
|
||||||
OPCODE_PING,
|
|
||||||
OPCODE_PONG,
|
|
||||||
)
|
|
||||||
|
|
||||||
# opcode human readable string
|
|
||||||
OPCODE_MAP = {
|
|
||||||
OPCODE_CONT: "cont",
|
|
||||||
OPCODE_TEXT: "text",
|
|
||||||
OPCODE_BINARY: "binary",
|
|
||||||
OPCODE_CLOSE: "close",
|
|
||||||
OPCODE_PING: "ping",
|
|
||||||
OPCODE_PONG: "pong",
|
|
||||||
}
|
|
||||||
|
|
||||||
# data length threshold.
|
|
||||||
LENGTH_7 = 0x7E
|
|
||||||
LENGTH_16 = 1 << 16
|
|
||||||
LENGTH_63 = 1 << 63
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
fin: int = 0,
|
|
||||||
rsv1: int = 0,
|
|
||||||
rsv2: int = 0,
|
|
||||||
rsv3: int = 0,
|
|
||||||
opcode: int = OPCODE_TEXT,
|
|
||||||
mask_value: int = 1,
|
|
||||||
data: Union[str, bytes, None] = "",
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Constructor for ABNF. Please check RFC for arguments.
|
|
||||||
"""
|
|
||||||
self.fin = fin
|
|
||||||
self.rsv1 = rsv1
|
|
||||||
self.rsv2 = rsv2
|
|
||||||
self.rsv3 = rsv3
|
|
||||||
self.opcode = opcode
|
|
||||||
self.mask_value = mask_value
|
|
||||||
if data is None:
|
|
||||||
data = ""
|
|
||||||
self.data = data
|
|
||||||
self.get_mask_key = os.urandom
|
|
||||||
|
|
||||||
def validate(self, skip_utf8_validation: bool = False) -> None:
|
|
||||||
"""
|
|
||||||
Validate the ABNF frame.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
skip_utf8_validation: skip utf8 validation.
|
|
||||||
"""
|
|
||||||
if self.rsv1 or self.rsv2 or self.rsv3:
|
|
||||||
raise WebSocketProtocolException("rsv is not implemented, yet")
|
|
||||||
|
|
||||||
if self.opcode not in ABNF.OPCODES:
|
|
||||||
raise WebSocketProtocolException("Invalid opcode %r", self.opcode)
|
|
||||||
|
|
||||||
if self.opcode == ABNF.OPCODE_PING and not self.fin:
|
|
||||||
raise WebSocketProtocolException("Invalid ping frame.")
|
|
||||||
|
|
||||||
if self.opcode == ABNF.OPCODE_CLOSE:
|
|
||||||
l = len(self.data)
|
|
||||||
if not l:
|
|
||||||
return
|
|
||||||
if l == 1 or l >= 126:
|
|
||||||
raise WebSocketProtocolException("Invalid close frame.")
|
|
||||||
if l > 2 and not skip_utf8_validation and not validate_utf8(self.data[2:]):
|
|
||||||
raise WebSocketProtocolException("Invalid close frame.")
|
|
||||||
|
|
||||||
code = 256 * int(self.data[0]) + int(self.data[1])
|
|
||||||
if not self._is_valid_close_status(code):
|
|
||||||
raise WebSocketProtocolException("Invalid close opcode %r", code)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _is_valid_close_status(code: int) -> bool:
|
|
||||||
return code in VALID_CLOSE_STATUS or (3000 <= code < 5000)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"fin={self.fin} opcode={self.opcode} data={self.data}"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def create_frame(data: Union[bytes, str], opcode: int, fin: int = 1) -> "ABNF":
|
|
||||||
"""
|
|
||||||
Create frame to send text, binary and other data.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
data: str
|
|
||||||
data to send. This is string value(byte array).
|
|
||||||
If opcode is OPCODE_TEXT and this value is unicode,
|
|
||||||
data value is converted into unicode string, automatically.
|
|
||||||
opcode: int
|
|
||||||
operation code. please see OPCODE_MAP.
|
|
||||||
fin: int
|
|
||||||
fin flag. if set to 0, create continue fragmentation.
|
|
||||||
"""
|
|
||||||
if opcode == ABNF.OPCODE_TEXT and isinstance(data, str):
|
|
||||||
data = data.encode("utf-8")
|
|
||||||
# mask must be set if send data from client
|
|
||||||
return ABNF(fin, 0, 0, 0, opcode, 1, data)
|
|
||||||
|
|
||||||
def format(self) -> bytes:
|
|
||||||
"""
|
|
||||||
Format this object to string(byte array) to send data to server.
|
|
||||||
"""
|
|
||||||
if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]):
|
|
||||||
raise ValueError("not 0 or 1")
|
|
||||||
if self.opcode not in ABNF.OPCODES:
|
|
||||||
raise ValueError("Invalid OPCODE")
|
|
||||||
length = len(self.data)
|
|
||||||
if length >= ABNF.LENGTH_63:
|
|
||||||
raise ValueError("data is too long")
|
|
||||||
|
|
||||||
frame_header = chr(
|
|
||||||
self.fin << 7
|
|
||||||
| self.rsv1 << 6
|
|
||||||
| self.rsv2 << 5
|
|
||||||
| self.rsv3 << 4
|
|
||||||
| self.opcode
|
|
||||||
).encode("latin-1")
|
|
||||||
if length < ABNF.LENGTH_7:
|
|
||||||
frame_header += chr(self.mask_value << 7 | length).encode("latin-1")
|
|
||||||
elif length < ABNF.LENGTH_16:
|
|
||||||
frame_header += chr(self.mask_value << 7 | 0x7E).encode("latin-1")
|
|
||||||
frame_header += struct.pack("!H", length)
|
|
||||||
else:
|
|
||||||
frame_header += chr(self.mask_value << 7 | 0x7F).encode("latin-1")
|
|
||||||
frame_header += struct.pack("!Q", length)
|
|
||||||
|
|
||||||
if not self.mask_value:
|
|
||||||
if isinstance(self.data, str):
|
|
||||||
self.data = self.data.encode("utf-8")
|
|
||||||
return frame_header + self.data
|
|
||||||
mask_key = self.get_mask_key(4)
|
|
||||||
return frame_header + self._get_masked(mask_key)
|
|
||||||
|
|
||||||
def _get_masked(self, mask_key: Union[str, bytes]) -> bytes:
|
|
||||||
s = ABNF.mask(mask_key, self.data)
|
|
||||||
|
|
||||||
if isinstance(mask_key, str):
|
|
||||||
mask_key = mask_key.encode("utf-8")
|
|
||||||
|
|
||||||
return mask_key + s
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def mask(mask_key: Union[str, bytes], data: Union[str, bytes]) -> bytes:
|
|
||||||
"""
|
|
||||||
Mask or unmask data. Just do xor for each byte
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
mask_key: bytes or str
|
|
||||||
4 byte mask.
|
|
||||||
data: bytes or str
|
|
||||||
data to mask/unmask.
|
|
||||||
"""
|
|
||||||
if data is None:
|
|
||||||
data = ""
|
|
||||||
|
|
||||||
if isinstance(mask_key, str):
|
|
||||||
mask_key = mask_key.encode("latin-1")
|
|
||||||
|
|
||||||
if isinstance(data, str):
|
|
||||||
data = data.encode("latin-1")
|
|
||||||
|
|
||||||
return _mask(array.array("B", mask_key), array.array("B", data))
|
|
||||||
|
|
||||||
|
|
||||||
class frame_buffer:
|
|
||||||
_HEADER_MASK_INDEX = 5
|
|
||||||
_HEADER_LENGTH_INDEX = 6
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self, recv_fn: Callable[[int], int], skip_utf8_validation: bool
|
|
||||||
) -> None:
|
|
||||||
self.recv = recv_fn
|
|
||||||
self.skip_utf8_validation = skip_utf8_validation
|
|
||||||
# Buffers over the packets from the layer beneath until desired amount
|
|
||||||
# bytes of bytes are received.
|
|
||||||
self.recv_buffer: list = []
|
|
||||||
self.clear()
|
|
||||||
self.lock = Lock()
|
|
||||||
|
|
||||||
def clear(self) -> None:
|
|
||||||
self.header: Optional[tuple] = None
|
|
||||||
self.length: Optional[int] = None
|
|
||||||
self.mask_value: Union[bytes, str, None] = None
|
|
||||||
|
|
||||||
def has_received_header(self) -> bool:
|
|
||||||
return self.header is None
|
|
||||||
|
|
||||||
def recv_header(self) -> None:
|
|
||||||
header = self.recv_strict(2)
|
|
||||||
b1 = header[0]
|
|
||||||
fin = b1 >> 7 & 1
|
|
||||||
rsv1 = b1 >> 6 & 1
|
|
||||||
rsv2 = b1 >> 5 & 1
|
|
||||||
rsv3 = b1 >> 4 & 1
|
|
||||||
opcode = b1 & 0xF
|
|
||||||
b2 = header[1]
|
|
||||||
has_mask = b2 >> 7 & 1
|
|
||||||
length_bits = b2 & 0x7F
|
|
||||||
|
|
||||||
self.header = (fin, rsv1, rsv2, rsv3, opcode, has_mask, length_bits)
|
|
||||||
|
|
||||||
def has_mask(self) -> Union[bool, int]:
|
|
||||||
if not self.header:
|
|
||||||
return False
|
|
||||||
header_val: int = self.header[frame_buffer._HEADER_MASK_INDEX]
|
|
||||||
return header_val
|
|
||||||
|
|
||||||
def has_received_length(self) -> bool:
|
|
||||||
return self.length is None
|
|
||||||
|
|
||||||
def recv_length(self) -> None:
|
|
||||||
bits = self.header[frame_buffer._HEADER_LENGTH_INDEX]
|
|
||||||
length_bits = bits & 0x7F
|
|
||||||
if length_bits == 0x7E:
|
|
||||||
v = self.recv_strict(2)
|
|
||||||
self.length = struct.unpack("!H", v)[0]
|
|
||||||
elif length_bits == 0x7F:
|
|
||||||
v = self.recv_strict(8)
|
|
||||||
self.length = struct.unpack("!Q", v)[0]
|
|
||||||
else:
|
|
||||||
self.length = length_bits
|
|
||||||
|
|
||||||
def has_received_mask(self) -> bool:
|
|
||||||
return self.mask_value is None
|
|
||||||
|
|
||||||
def recv_mask(self) -> None:
|
|
||||||
self.mask_value = self.recv_strict(4) if self.has_mask() else ""
|
|
||||||
|
|
||||||
def recv_frame(self) -> ABNF:
|
|
||||||
with self.lock:
|
|
||||||
# Header
|
|
||||||
if self.has_received_header():
|
|
||||||
self.recv_header()
|
|
||||||
(fin, rsv1, rsv2, rsv3, opcode, has_mask, _) = self.header
|
|
||||||
|
|
||||||
# Frame length
|
|
||||||
if self.has_received_length():
|
|
||||||
self.recv_length()
|
|
||||||
length = self.length
|
|
||||||
|
|
||||||
# Mask
|
|
||||||
if self.has_received_mask():
|
|
||||||
self.recv_mask()
|
|
||||||
mask_value = self.mask_value
|
|
||||||
|
|
||||||
# Payload
|
|
||||||
payload = self.recv_strict(length)
|
|
||||||
if has_mask:
|
|
||||||
payload = ABNF.mask(mask_value, payload)
|
|
||||||
|
|
||||||
# Reset for next frame
|
|
||||||
self.clear()
|
|
||||||
|
|
||||||
frame = ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload)
|
|
||||||
frame.validate(self.skip_utf8_validation)
|
|
||||||
|
|
||||||
return frame
|
|
||||||
|
|
||||||
def recv_strict(self, bufsize: int) -> bytes:
|
|
||||||
shortage = bufsize - sum(map(len, self.recv_buffer))
|
|
||||||
while shortage > 0:
|
|
||||||
# Limit buffer size that we pass to socket.recv() to avoid
|
|
||||||
# fragmenting the heap -- the number of bytes recv() actually
|
|
||||||
# reads is limited by socket buffer and is relatively small,
|
|
||||||
# yet passing large numbers repeatedly causes lots of large
|
|
||||||
# buffers allocated and then shrunk, which results in
|
|
||||||
# fragmentation.
|
|
||||||
bytes_ = self.recv(min(16384, shortage))
|
|
||||||
self.recv_buffer.append(bytes_)
|
|
||||||
shortage -= len(bytes_)
|
|
||||||
|
|
||||||
unified = b"".join(self.recv_buffer)
|
|
||||||
|
|
||||||
if shortage == 0:
|
|
||||||
self.recv_buffer = []
|
|
||||||
return unified
|
|
||||||
else:
|
|
||||||
self.recv_buffer = [unified[bufsize:]]
|
|
||||||
return unified[:bufsize]
|
|
||||||
|
|
||||||
|
|
||||||
class continuous_frame:
|
|
||||||
def __init__(self, fire_cont_frame: bool, skip_utf8_validation: bool) -> None:
|
|
||||||
self.fire_cont_frame = fire_cont_frame
|
|
||||||
self.skip_utf8_validation = skip_utf8_validation
|
|
||||||
self.cont_data: Optional[list] = None
|
|
||||||
self.recving_frames: Optional[int] = None
|
|
||||||
|
|
||||||
def validate(self, frame: ABNF) -> None:
|
|
||||||
if not self.recving_frames and frame.opcode == ABNF.OPCODE_CONT:
|
|
||||||
raise WebSocketProtocolException("Illegal frame")
|
|
||||||
if self.recving_frames and frame.opcode in (
|
|
||||||
ABNF.OPCODE_TEXT,
|
|
||||||
ABNF.OPCODE_BINARY,
|
|
||||||
):
|
|
||||||
raise WebSocketProtocolException("Illegal frame")
|
|
||||||
|
|
||||||
def add(self, frame: ABNF) -> None:
|
|
||||||
if self.cont_data:
|
|
||||||
self.cont_data[1] += frame.data
|
|
||||||
else:
|
|
||||||
if frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY):
|
|
||||||
self.recving_frames = frame.opcode
|
|
||||||
self.cont_data = [frame.opcode, frame.data]
|
|
||||||
|
|
||||||
if frame.fin:
|
|
||||||
self.recving_frames = None
|
|
||||||
|
|
||||||
def is_fire(self, frame: ABNF) -> Union[bool, int]:
|
|
||||||
return frame.fin or self.fire_cont_frame
|
|
||||||
|
|
||||||
def extract(self, frame: ABNF) -> tuple:
|
|
||||||
data = self.cont_data
|
|
||||||
self.cont_data = None
|
|
||||||
frame.data = data[1]
|
|
||||||
if (
|
|
||||||
not self.fire_cont_frame
|
|
||||||
and data[0] == ABNF.OPCODE_TEXT
|
|
||||||
and not self.skip_utf8_validation
|
|
||||||
and not validate_utf8(frame.data)
|
|
||||||
):
|
|
||||||
raise WebSocketPayloadException(f"cannot decode: {repr(frame.data)}")
|
|
||||||
return data[0], frame
|
|
||||||
@@ -1,677 +0,0 @@
|
|||||||
import inspect
|
|
||||||
import selectors
|
|
||||||
import socket
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from typing import Any, Callable, Optional, Union
|
|
||||||
|
|
||||||
from . import _logging
|
|
||||||
from ._abnf import ABNF
|
|
||||||
from ._core import WebSocket, getdefaulttimeout
|
|
||||||
from ._exceptions import (
|
|
||||||
WebSocketConnectionClosedException,
|
|
||||||
WebSocketException,
|
|
||||||
WebSocketTimeoutException,
|
|
||||||
)
|
|
||||||
from ._ssl_compat import SSLEOFError
|
|
||||||
from ._url import parse_url
|
|
||||||
|
|
||||||
"""
|
|
||||||
_app.py
|
|
||||||
websocket - WebSocket client library for Python
|
|
||||||
|
|
||||||
Copyright 2024 engn33r
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__all__ = ["WebSocketApp"]
|
|
||||||
|
|
||||||
RECONNECT = 0
|
|
||||||
|
|
||||||
|
|
||||||
def setReconnect(reconnectInterval: int) -> None:
|
|
||||||
global RECONNECT
|
|
||||||
RECONNECT = reconnectInterval
|
|
||||||
|
|
||||||
|
|
||||||
class DispatcherBase:
|
|
||||||
"""
|
|
||||||
DispatcherBase
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, app: Any, ping_timeout: Union[float, int, None]) -> None:
|
|
||||||
self.app = app
|
|
||||||
self.ping_timeout = ping_timeout
|
|
||||||
|
|
||||||
def timeout(self, seconds: Union[float, int, None], callback: Callable) -> None:
|
|
||||||
time.sleep(seconds)
|
|
||||||
callback()
|
|
||||||
|
|
||||||
def reconnect(self, seconds: int, reconnector: Callable) -> None:
|
|
||||||
try:
|
|
||||||
_logging.info(
|
|
||||||
f"reconnect() - retrying in {seconds} seconds [{len(inspect.stack())} frames in stack]"
|
|
||||||
)
|
|
||||||
time.sleep(seconds)
|
|
||||||
reconnector(reconnecting=True)
|
|
||||||
except KeyboardInterrupt as e:
|
|
||||||
_logging.info(f"User exited {e}")
|
|
||||||
raise e
|
|
||||||
|
|
||||||
|
|
||||||
class Dispatcher(DispatcherBase):
|
|
||||||
"""
|
|
||||||
Dispatcher
|
|
||||||
"""
|
|
||||||
|
|
||||||
def read(
|
|
||||||
self,
|
|
||||||
sock: socket.socket,
|
|
||||||
read_callback: Callable,
|
|
||||||
check_callback: Callable,
|
|
||||||
) -> None:
|
|
||||||
sel = selectors.DefaultSelector()
|
|
||||||
sel.register(self.app.sock.sock, selectors.EVENT_READ)
|
|
||||||
try:
|
|
||||||
while self.app.keep_running:
|
|
||||||
if sel.select(self.ping_timeout):
|
|
||||||
if not read_callback():
|
|
||||||
break
|
|
||||||
check_callback()
|
|
||||||
finally:
|
|
||||||
sel.close()
|
|
||||||
|
|
||||||
|
|
||||||
class SSLDispatcher(DispatcherBase):
|
|
||||||
"""
|
|
||||||
SSLDispatcher
|
|
||||||
"""
|
|
||||||
|
|
||||||
def read(
|
|
||||||
self,
|
|
||||||
sock: socket.socket,
|
|
||||||
read_callback: Callable,
|
|
||||||
check_callback: Callable,
|
|
||||||
) -> None:
|
|
||||||
sock = self.app.sock.sock
|
|
||||||
sel = selectors.DefaultSelector()
|
|
||||||
sel.register(sock, selectors.EVENT_READ)
|
|
||||||
try:
|
|
||||||
while self.app.keep_running:
|
|
||||||
if self.select(sock, sel):
|
|
||||||
if not read_callback():
|
|
||||||
break
|
|
||||||
check_callback()
|
|
||||||
finally:
|
|
||||||
sel.close()
|
|
||||||
|
|
||||||
def select(self, sock, sel: selectors.DefaultSelector):
|
|
||||||
sock = self.app.sock.sock
|
|
||||||
if sock.pending():
|
|
||||||
return [
|
|
||||||
sock,
|
|
||||||
]
|
|
||||||
|
|
||||||
r = sel.select(self.ping_timeout)
|
|
||||||
|
|
||||||
if len(r) > 0:
|
|
||||||
return r[0][0]
|
|
||||||
|
|
||||||
|
|
||||||
class WrappedDispatcher:
|
|
||||||
"""
|
|
||||||
WrappedDispatcher
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, app, ping_timeout: Union[float, int, None], dispatcher) -> None:
|
|
||||||
self.app = app
|
|
||||||
self.ping_timeout = ping_timeout
|
|
||||||
self.dispatcher = dispatcher
|
|
||||||
dispatcher.signal(2, dispatcher.abort) # keyboard interrupt
|
|
||||||
|
|
||||||
def read(
|
|
||||||
self,
|
|
||||||
sock: socket.socket,
|
|
||||||
read_callback: Callable,
|
|
||||||
check_callback: Callable,
|
|
||||||
) -> None:
|
|
||||||
self.dispatcher.read(sock, read_callback)
|
|
||||||
self.ping_timeout and self.timeout(self.ping_timeout, check_callback)
|
|
||||||
|
|
||||||
def timeout(self, seconds: float, callback: Callable) -> None:
|
|
||||||
self.dispatcher.timeout(seconds, callback)
|
|
||||||
|
|
||||||
def reconnect(self, seconds: int, reconnector: Callable) -> None:
|
|
||||||
self.timeout(seconds, reconnector)
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketApp:
|
|
||||||
"""
|
|
||||||
Higher level of APIs are provided. The interface is like JavaScript WebSocket object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
url: str,
|
|
||||||
header: Union[list, dict, Callable, None] = None,
|
|
||||||
on_open: Optional[Callable[[WebSocket], None]] = None,
|
|
||||||
on_reconnect: Optional[Callable[[WebSocket], None]] = None,
|
|
||||||
on_message: Optional[Callable[[WebSocket, Any], None]] = None,
|
|
||||||
on_error: Optional[Callable[[WebSocket, Any], None]] = None,
|
|
||||||
on_close: Optional[Callable[[WebSocket, Any, Any], None]] = None,
|
|
||||||
on_ping: Optional[Callable] = None,
|
|
||||||
on_pong: Optional[Callable] = None,
|
|
||||||
on_cont_message: Optional[Callable] = None,
|
|
||||||
keep_running: bool = True,
|
|
||||||
get_mask_key: Optional[Callable] = None,
|
|
||||||
cookie: Optional[str] = None,
|
|
||||||
subprotocols: Optional[list] = None,
|
|
||||||
on_data: Optional[Callable] = None,
|
|
||||||
socket: Optional[socket.socket] = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
WebSocketApp initialization
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
url: str
|
|
||||||
Websocket url.
|
|
||||||
header: list or dict or Callable
|
|
||||||
Custom header for websocket handshake.
|
|
||||||
If the parameter is a callable object, it is called just before the connection attempt.
|
|
||||||
The returned dict or list is used as custom header value.
|
|
||||||
This could be useful in order to properly setup timestamp dependent headers.
|
|
||||||
on_open: function
|
|
||||||
Callback object which is called at opening websocket.
|
|
||||||
on_open has one argument.
|
|
||||||
The 1st argument is this class object.
|
|
||||||
on_reconnect: function
|
|
||||||
Callback object which is called at reconnecting websocket.
|
|
||||||
on_reconnect has one argument.
|
|
||||||
The 1st argument is this class object.
|
|
||||||
on_message: function
|
|
||||||
Callback object which is called when received data.
|
|
||||||
on_message has 2 arguments.
|
|
||||||
The 1st argument is this class object.
|
|
||||||
The 2nd argument is utf-8 data received from the server.
|
|
||||||
on_error: function
|
|
||||||
Callback object which is called when we get error.
|
|
||||||
on_error has 2 arguments.
|
|
||||||
The 1st argument is this class object.
|
|
||||||
The 2nd argument is exception object.
|
|
||||||
on_close: function
|
|
||||||
Callback object which is called when connection is closed.
|
|
||||||
on_close has 3 arguments.
|
|
||||||
The 1st argument is this class object.
|
|
||||||
The 2nd argument is close_status_code.
|
|
||||||
The 3rd argument is close_msg.
|
|
||||||
on_cont_message: function
|
|
||||||
Callback object which is called when a continuation
|
|
||||||
frame is received.
|
|
||||||
on_cont_message has 3 arguments.
|
|
||||||
The 1st argument is this class object.
|
|
||||||
The 2nd argument is utf-8 string which we get from the server.
|
|
||||||
The 3rd argument is continue flag. if 0, the data continue
|
|
||||||
to next frame data
|
|
||||||
on_data: function
|
|
||||||
Callback object which is called when a message received.
|
|
||||||
This is called before on_message or on_cont_message,
|
|
||||||
and then on_message or on_cont_message is called.
|
|
||||||
on_data has 4 argument.
|
|
||||||
The 1st argument is this class object.
|
|
||||||
The 2nd argument is utf-8 string which we get from the server.
|
|
||||||
The 3rd argument is data type. ABNF.OPCODE_TEXT or ABNF.OPCODE_BINARY will be came.
|
|
||||||
The 4th argument is continue flag. If 0, the data continue
|
|
||||||
keep_running: bool
|
|
||||||
This parameter is obsolete and ignored.
|
|
||||||
get_mask_key: function
|
|
||||||
A callable function to get new mask keys, see the
|
|
||||||
WebSocket.set_mask_key's docstring for more information.
|
|
||||||
cookie: str
|
|
||||||
Cookie value.
|
|
||||||
subprotocols: list
|
|
||||||
List of available sub protocols. Default is None.
|
|
||||||
socket: socket
|
|
||||||
Pre-initialized stream socket.
|
|
||||||
"""
|
|
||||||
self.url = url
|
|
||||||
self.header = header if header is not None else []
|
|
||||||
self.cookie = cookie
|
|
||||||
|
|
||||||
self.on_open = on_open
|
|
||||||
self.on_reconnect = on_reconnect
|
|
||||||
self.on_message = on_message
|
|
||||||
self.on_data = on_data
|
|
||||||
self.on_error = on_error
|
|
||||||
self.on_close = on_close
|
|
||||||
self.on_ping = on_ping
|
|
||||||
self.on_pong = on_pong
|
|
||||||
self.on_cont_message = on_cont_message
|
|
||||||
self.keep_running = False
|
|
||||||
self.get_mask_key = get_mask_key
|
|
||||||
self.sock: Optional[WebSocket] = None
|
|
||||||
self.last_ping_tm = float(0)
|
|
||||||
self.last_pong_tm = float(0)
|
|
||||||
self.ping_thread: Optional[threading.Thread] = None
|
|
||||||
self.stop_ping: Optional[threading.Event] = None
|
|
||||||
self.ping_interval = float(0)
|
|
||||||
self.ping_timeout: Union[float, int, None] = None
|
|
||||||
self.ping_payload = ""
|
|
||||||
self.subprotocols = subprotocols
|
|
||||||
self.prepared_socket = socket
|
|
||||||
self.has_errored = False
|
|
||||||
self.has_done_teardown = False
|
|
||||||
self.has_done_teardown_lock = threading.Lock()
|
|
||||||
|
|
||||||
def send(self, data: Union[bytes, str], opcode: int = ABNF.OPCODE_TEXT) -> None:
|
|
||||||
"""
|
|
||||||
send message
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
data: str
|
|
||||||
Message to send. If you set opcode to OPCODE_TEXT,
|
|
||||||
data must be utf-8 string or unicode.
|
|
||||||
opcode: int
|
|
||||||
Operation code of data. Default is OPCODE_TEXT.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not self.sock or self.sock.send(data, opcode) == 0:
|
|
||||||
raise WebSocketConnectionClosedException("Connection is already closed.")
|
|
||||||
|
|
||||||
def send_text(self, text_data: str) -> None:
|
|
||||||
"""
|
|
||||||
Sends UTF-8 encoded text.
|
|
||||||
"""
|
|
||||||
if not self.sock or self.sock.send(text_data, ABNF.OPCODE_TEXT) == 0:
|
|
||||||
raise WebSocketConnectionClosedException("Connection is already closed.")
|
|
||||||
|
|
||||||
def send_bytes(self, data: Union[bytes, bytearray]) -> None:
|
|
||||||
"""
|
|
||||||
Sends a sequence of bytes.
|
|
||||||
"""
|
|
||||||
if not self.sock or self.sock.send(data, ABNF.OPCODE_BINARY) == 0:
|
|
||||||
raise WebSocketConnectionClosedException("Connection is already closed.")
|
|
||||||
|
|
||||||
def close(self, **kwargs) -> None:
|
|
||||||
"""
|
|
||||||
Close websocket connection.
|
|
||||||
"""
|
|
||||||
self.keep_running = False
|
|
||||||
if self.sock:
|
|
||||||
self.sock.close(**kwargs)
|
|
||||||
self.sock = None
|
|
||||||
|
|
||||||
def _start_ping_thread(self) -> None:
|
|
||||||
self.last_ping_tm = self.last_pong_tm = float(0)
|
|
||||||
self.stop_ping = threading.Event()
|
|
||||||
self.ping_thread = threading.Thread(target=self._send_ping)
|
|
||||||
self.ping_thread.daemon = True
|
|
||||||
self.ping_thread.start()
|
|
||||||
|
|
||||||
def _stop_ping_thread(self) -> None:
|
|
||||||
if self.stop_ping:
|
|
||||||
self.stop_ping.set()
|
|
||||||
if self.ping_thread and self.ping_thread.is_alive():
|
|
||||||
self.ping_thread.join(3)
|
|
||||||
self.last_ping_tm = self.last_pong_tm = float(0)
|
|
||||||
|
|
||||||
def _send_ping(self) -> None:
|
|
||||||
if self.stop_ping.wait(self.ping_interval) or self.keep_running is False:
|
|
||||||
return
|
|
||||||
while not self.stop_ping.wait(self.ping_interval) and self.keep_running is True:
|
|
||||||
if self.sock:
|
|
||||||
self.last_ping_tm = time.time()
|
|
||||||
try:
|
|
||||||
_logging.debug("Sending ping")
|
|
||||||
self.sock.ping(self.ping_payload)
|
|
||||||
except Exception as e:
|
|
||||||
_logging.debug(f"Failed to send ping: {e}")
|
|
||||||
|
|
||||||
def run_forever(
|
|
||||||
self,
|
|
||||||
sockopt: tuple = None,
|
|
||||||
sslopt: dict = None,
|
|
||||||
ping_interval: Union[float, int] = 0,
|
|
||||||
ping_timeout: Union[float, int, None] = None,
|
|
||||||
ping_payload: str = "",
|
|
||||||
http_proxy_host: str = None,
|
|
||||||
http_proxy_port: Union[int, str] = None,
|
|
||||||
http_no_proxy: list = None,
|
|
||||||
http_proxy_auth: tuple = None,
|
|
||||||
http_proxy_timeout: Optional[float] = None,
|
|
||||||
skip_utf8_validation: bool = False,
|
|
||||||
host: str = None,
|
|
||||||
origin: str = None,
|
|
||||||
dispatcher=None,
|
|
||||||
suppress_origin: bool = False,
|
|
||||||
proxy_type: str = None,
|
|
||||||
reconnect: int = None,
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
Run event loop for WebSocket framework.
|
|
||||||
|
|
||||||
This loop is an infinite loop and is alive while websocket is available.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
sockopt: tuple
|
|
||||||
Values for socket.setsockopt.
|
|
||||||
sockopt must be tuple
|
|
||||||
and each element is argument of sock.setsockopt.
|
|
||||||
sslopt: dict
|
|
||||||
Optional dict object for ssl socket option.
|
|
||||||
ping_interval: int or float
|
|
||||||
Automatically send "ping" command
|
|
||||||
every specified period (in seconds).
|
|
||||||
If set to 0, no ping is sent periodically.
|
|
||||||
ping_timeout: int or float
|
|
||||||
Timeout (in seconds) if the pong message is not received.
|
|
||||||
ping_payload: str
|
|
||||||
Payload message to send with each ping.
|
|
||||||
http_proxy_host: str
|
|
||||||
HTTP proxy host name.
|
|
||||||
http_proxy_port: int or str
|
|
||||||
HTTP proxy port. If not set, set to 80.
|
|
||||||
http_no_proxy: list
|
|
||||||
Whitelisted host names that don't use the proxy.
|
|
||||||
http_proxy_timeout: int or float
|
|
||||||
HTTP proxy timeout, default is 60 sec as per python-socks.
|
|
||||||
http_proxy_auth: tuple
|
|
||||||
HTTP proxy auth information. tuple of username and password. Default is None.
|
|
||||||
skip_utf8_validation: bool
|
|
||||||
skip utf8 validation.
|
|
||||||
host: str
|
|
||||||
update host header.
|
|
||||||
origin: str
|
|
||||||
update origin header.
|
|
||||||
dispatcher: Dispatcher object
|
|
||||||
customize reading data from socket.
|
|
||||||
suppress_origin: bool
|
|
||||||
suppress outputting origin header.
|
|
||||||
proxy_type: str
|
|
||||||
type of proxy from: http, socks4, socks4a, socks5, socks5h
|
|
||||||
reconnect: int
|
|
||||||
delay interval when reconnecting
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
teardown: bool
|
|
||||||
False if the `WebSocketApp` is closed or caught KeyboardInterrupt,
|
|
||||||
True if any other exception was raised during a loop.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if reconnect is None:
|
|
||||||
reconnect = RECONNECT
|
|
||||||
|
|
||||||
if ping_timeout is not None and ping_timeout <= 0:
|
|
||||||
raise WebSocketException("Ensure ping_timeout > 0")
|
|
||||||
if ping_interval is not None and ping_interval < 0:
|
|
||||||
raise WebSocketException("Ensure ping_interval >= 0")
|
|
||||||
if ping_timeout and ping_interval and ping_interval <= ping_timeout:
|
|
||||||
raise WebSocketException("Ensure ping_interval > ping_timeout")
|
|
||||||
if not sockopt:
|
|
||||||
sockopt = ()
|
|
||||||
if not sslopt:
|
|
||||||
sslopt = {}
|
|
||||||
if self.sock:
|
|
||||||
raise WebSocketException("socket is already opened")
|
|
||||||
|
|
||||||
self.ping_interval = ping_interval
|
|
||||||
self.ping_timeout = ping_timeout
|
|
||||||
self.ping_payload = ping_payload
|
|
||||||
self.has_done_teardown = False
|
|
||||||
self.keep_running = True
|
|
||||||
|
|
||||||
def teardown(close_frame: ABNF = None):
|
|
||||||
"""
|
|
||||||
Tears down the connection.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
close_frame: ABNF frame
|
|
||||||
If close_frame is set, the on_close handler is invoked
|
|
||||||
with the statusCode and reason from the provided frame.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# teardown() is called in many code paths to ensure resources are cleaned up and on_close is fired.
|
|
||||||
# To ensure the work is only done once, we use this bool and lock.
|
|
||||||
with self.has_done_teardown_lock:
|
|
||||||
if self.has_done_teardown:
|
|
||||||
return
|
|
||||||
self.has_done_teardown = True
|
|
||||||
|
|
||||||
self._stop_ping_thread()
|
|
||||||
self.keep_running = False
|
|
||||||
if self.sock:
|
|
||||||
self.sock.close()
|
|
||||||
close_status_code, close_reason = self._get_close_args(
|
|
||||||
close_frame if close_frame else None
|
|
||||||
)
|
|
||||||
self.sock = None
|
|
||||||
|
|
||||||
# Finally call the callback AFTER all teardown is complete
|
|
||||||
self._callback(self.on_close, close_status_code, close_reason)
|
|
||||||
|
|
||||||
def setSock(reconnecting: bool = False) -> None:
|
|
||||||
if reconnecting and self.sock:
|
|
||||||
self.sock.shutdown()
|
|
||||||
|
|
||||||
self.sock = WebSocket(
|
|
||||||
self.get_mask_key,
|
|
||||||
sockopt=sockopt,
|
|
||||||
sslopt=sslopt,
|
|
||||||
fire_cont_frame=self.on_cont_message is not None,
|
|
||||||
skip_utf8_validation=skip_utf8_validation,
|
|
||||||
enable_multithread=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sock.settimeout(getdefaulttimeout())
|
|
||||||
try:
|
|
||||||
header = self.header() if callable(self.header) else self.header
|
|
||||||
|
|
||||||
self.sock.connect(
|
|
||||||
self.url,
|
|
||||||
header=header,
|
|
||||||
cookie=self.cookie,
|
|
||||||
http_proxy_host=http_proxy_host,
|
|
||||||
http_proxy_port=http_proxy_port,
|
|
||||||
http_no_proxy=http_no_proxy,
|
|
||||||
http_proxy_auth=http_proxy_auth,
|
|
||||||
http_proxy_timeout=http_proxy_timeout,
|
|
||||||
subprotocols=self.subprotocols,
|
|
||||||
host=host,
|
|
||||||
origin=origin,
|
|
||||||
suppress_origin=suppress_origin,
|
|
||||||
proxy_type=proxy_type,
|
|
||||||
socket=self.prepared_socket,
|
|
||||||
)
|
|
||||||
|
|
||||||
_logging.info("Websocket connected")
|
|
||||||
|
|
||||||
if self.ping_interval:
|
|
||||||
self._start_ping_thread()
|
|
||||||
|
|
||||||
if reconnecting and self.on_reconnect:
|
|
||||||
self._callback(self.on_reconnect)
|
|
||||||
else:
|
|
||||||
self._callback(self.on_open)
|
|
||||||
|
|
||||||
dispatcher.read(self.sock.sock, read, check)
|
|
||||||
except (
|
|
||||||
WebSocketConnectionClosedException,
|
|
||||||
ConnectionRefusedError,
|
|
||||||
KeyboardInterrupt,
|
|
||||||
SystemExit,
|
|
||||||
Exception,
|
|
||||||
) as e:
|
|
||||||
handleDisconnect(e, reconnecting)
|
|
||||||
|
|
||||||
def read() -> bool:
|
|
||||||
if not self.keep_running:
|
|
||||||
return teardown()
|
|
||||||
|
|
||||||
try:
|
|
||||||
op_code, frame = self.sock.recv_data_frame(True)
|
|
||||||
except (
|
|
||||||
WebSocketConnectionClosedException,
|
|
||||||
KeyboardInterrupt,
|
|
||||||
SSLEOFError,
|
|
||||||
) as e:
|
|
||||||
if custom_dispatcher:
|
|
||||||
return handleDisconnect(e, bool(reconnect))
|
|
||||||
else:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
if op_code == ABNF.OPCODE_CLOSE:
|
|
||||||
return teardown(frame)
|
|
||||||
elif op_code == ABNF.OPCODE_PING:
|
|
||||||
self._callback(self.on_ping, frame.data)
|
|
||||||
elif op_code == ABNF.OPCODE_PONG:
|
|
||||||
self.last_pong_tm = time.time()
|
|
||||||
self._callback(self.on_pong, frame.data)
|
|
||||||
elif op_code == ABNF.OPCODE_CONT and self.on_cont_message:
|
|
||||||
self._callback(self.on_data, frame.data, frame.opcode, frame.fin)
|
|
||||||
self._callback(self.on_cont_message, frame.data, frame.fin)
|
|
||||||
else:
|
|
||||||
data = frame.data
|
|
||||||
if op_code == ABNF.OPCODE_TEXT and not skip_utf8_validation:
|
|
||||||
data = data.decode("utf-8")
|
|
||||||
self._callback(self.on_data, data, frame.opcode, True)
|
|
||||||
self._callback(self.on_message, data)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def check() -> bool:
|
|
||||||
if self.ping_timeout:
|
|
||||||
has_timeout_expired = (
|
|
||||||
time.time() - self.last_ping_tm > self.ping_timeout
|
|
||||||
)
|
|
||||||
has_pong_not_arrived_after_last_ping = (
|
|
||||||
self.last_pong_tm - self.last_ping_tm < 0
|
|
||||||
)
|
|
||||||
has_pong_arrived_too_late = (
|
|
||||||
self.last_pong_tm - self.last_ping_tm > self.ping_timeout
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
self.last_ping_tm
|
|
||||||
and has_timeout_expired
|
|
||||||
and (
|
|
||||||
has_pong_not_arrived_after_last_ping
|
|
||||||
or has_pong_arrived_too_late
|
|
||||||
)
|
|
||||||
):
|
|
||||||
raise WebSocketTimeoutException("ping/pong timed out")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def handleDisconnect(
|
|
||||||
e: Union[
|
|
||||||
WebSocketConnectionClosedException,
|
|
||||||
ConnectionRefusedError,
|
|
||||||
KeyboardInterrupt,
|
|
||||||
SystemExit,
|
|
||||||
Exception,
|
|
||||||
],
|
|
||||||
reconnecting: bool = False,
|
|
||||||
) -> bool:
|
|
||||||
self.has_errored = True
|
|
||||||
self._stop_ping_thread()
|
|
||||||
if not reconnecting:
|
|
||||||
self._callback(self.on_error, e)
|
|
||||||
|
|
||||||
if isinstance(e, (KeyboardInterrupt, SystemExit)):
|
|
||||||
teardown()
|
|
||||||
# Propagate further
|
|
||||||
raise
|
|
||||||
|
|
||||||
if reconnect:
|
|
||||||
_logging.info(f"{e} - reconnect")
|
|
||||||
if custom_dispatcher:
|
|
||||||
_logging.debug(
|
|
||||||
f"Calling custom dispatcher reconnect [{len(inspect.stack())} frames in stack]"
|
|
||||||
)
|
|
||||||
dispatcher.reconnect(reconnect, setSock)
|
|
||||||
else:
|
|
||||||
_logging.error(f"{e} - goodbye")
|
|
||||||
teardown()
|
|
||||||
|
|
||||||
custom_dispatcher = bool(dispatcher)
|
|
||||||
dispatcher = self.create_dispatcher(
|
|
||||||
ping_timeout, dispatcher, parse_url(self.url)[3]
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
setSock()
|
|
||||||
if not custom_dispatcher and reconnect:
|
|
||||||
while self.keep_running:
|
|
||||||
_logging.debug(
|
|
||||||
f"Calling dispatcher reconnect [{len(inspect.stack())} frames in stack]"
|
|
||||||
)
|
|
||||||
dispatcher.reconnect(reconnect, setSock)
|
|
||||||
except (KeyboardInterrupt, Exception) as e:
|
|
||||||
_logging.info(f"tearing down on exception {e}")
|
|
||||||
teardown()
|
|
||||||
finally:
|
|
||||||
if not custom_dispatcher:
|
|
||||||
# Ensure teardown was called before returning from run_forever
|
|
||||||
teardown()
|
|
||||||
|
|
||||||
return self.has_errored
|
|
||||||
|
|
||||||
def create_dispatcher(
|
|
||||||
self,
|
|
||||||
ping_timeout: Union[float, int, None],
|
|
||||||
dispatcher: Optional[DispatcherBase] = None,
|
|
||||||
is_ssl: bool = False,
|
|
||||||
) -> Union[Dispatcher, SSLDispatcher, WrappedDispatcher]:
|
|
||||||
if dispatcher: # If custom dispatcher is set, use WrappedDispatcher
|
|
||||||
return WrappedDispatcher(self, ping_timeout, dispatcher)
|
|
||||||
timeout = ping_timeout or 10
|
|
||||||
if is_ssl:
|
|
||||||
return SSLDispatcher(self, timeout)
|
|
||||||
return Dispatcher(self, timeout)
|
|
||||||
|
|
||||||
def _get_close_args(self, close_frame: ABNF) -> list:
|
|
||||||
"""
|
|
||||||
_get_close_args extracts the close code and reason from the close body
|
|
||||||
if it exists (RFC6455 says WebSocket Connection Close Code is optional)
|
|
||||||
"""
|
|
||||||
# Need to catch the case where close_frame is None
|
|
||||||
# Otherwise the following if statement causes an error
|
|
||||||
if not self.on_close or not close_frame:
|
|
||||||
return [None, None]
|
|
||||||
|
|
||||||
# Extract close frame status code
|
|
||||||
if close_frame.data and len(close_frame.data) >= 2:
|
|
||||||
close_status_code = 256 * int(close_frame.data[0]) + int(
|
|
||||||
close_frame.data[1]
|
|
||||||
)
|
|
||||||
reason = close_frame.data[2:]
|
|
||||||
if isinstance(reason, bytes):
|
|
||||||
reason = reason.decode("utf-8")
|
|
||||||
return [close_status_code, reason]
|
|
||||||
else:
|
|
||||||
# Most likely reached this because len(close_frame_data.data) < 2
|
|
||||||
return [None, None]
|
|
||||||
|
|
||||||
def _callback(self, callback, *args) -> None:
|
|
||||||
if callback:
|
|
||||||
try:
|
|
||||||
callback(self, *args)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
_logging.error(f"error from callback {callback}: {e}")
|
|
||||||
if self.on_error:
|
|
||||||
self.on_error(self, e)
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import http.cookies
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
"""
|
|
||||||
_cookiejar.py
|
|
||||||
websocket - WebSocket client library for Python
|
|
||||||
|
|
||||||
Copyright 2024 engn33r
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleCookieJar:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.jar: dict = {}
|
|
||||||
|
|
||||||
def add(self, set_cookie: Optional[str]) -> None:
|
|
||||||
if set_cookie:
|
|
||||||
simple_cookie = http.cookies.SimpleCookie(set_cookie)
|
|
||||||
|
|
||||||
for v in simple_cookie.values():
|
|
||||||
if domain := v.get("domain"):
|
|
||||||
if not domain.startswith("."):
|
|
||||||
domain = f".{domain}"
|
|
||||||
cookie = (
|
|
||||||
self.jar.get(domain)
|
|
||||||
if self.jar.get(domain)
|
|
||||||
else http.cookies.SimpleCookie()
|
|
||||||
)
|
|
||||||
cookie.update(simple_cookie)
|
|
||||||
self.jar[domain.lower()] = cookie
|
|
||||||
|
|
||||||
def set(self, set_cookie: str) -> None:
|
|
||||||
if set_cookie:
|
|
||||||
simple_cookie = http.cookies.SimpleCookie(set_cookie)
|
|
||||||
|
|
||||||
for v in simple_cookie.values():
|
|
||||||
if domain := v.get("domain"):
|
|
||||||
if not domain.startswith("."):
|
|
||||||
domain = f".{domain}"
|
|
||||||
self.jar[domain.lower()] = simple_cookie
|
|
||||||
|
|
||||||
def get(self, host: str) -> str:
|
|
||||||
if not host:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
cookies = []
|
|
||||||
for domain, _ in self.jar.items():
|
|
||||||
host = host.lower()
|
|
||||||
if host.endswith(domain) or host == domain[1:]:
|
|
||||||
cookies.append(self.jar.get(domain))
|
|
||||||
|
|
||||||
return "; ".join(
|
|
||||||
filter(
|
|
||||||
None,
|
|
||||||
sorted(
|
|
||||||
[
|
|
||||||
f"{k}={v.value}"
|
|
||||||
for cookie in filter(None, cookies)
|
|
||||||
for k, v in cookie.items()
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@@ -1,647 +0,0 @@
|
|||||||
import socket
|
|
||||||
import struct
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from typing import Optional, Union
|
|
||||||
|
|
||||||
# websocket modules
|
|
||||||
from ._abnf import ABNF, STATUS_NORMAL, continuous_frame, frame_buffer
|
|
||||||
from ._exceptions import WebSocketProtocolException, WebSocketConnectionClosedException
|
|
||||||
from ._handshake import SUPPORTED_REDIRECT_STATUSES, handshake
|
|
||||||
from ._http import connect, proxy_info
|
|
||||||
from ._logging import debug, error, trace, isEnabledForError, isEnabledForTrace
|
|
||||||
from ._socket import getdefaulttimeout, recv, send, sock_opt
|
|
||||||
from ._ssl_compat import ssl
|
|
||||||
from ._utils import NoLock
|
|
||||||
|
|
||||||
"""
|
|
||||||
_core.py
|
|
||||||
websocket - WebSocket client library for Python
|
|
||||||
|
|
||||||
Copyright 2024 engn33r
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__all__ = ["WebSocket", "create_connection"]
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocket:
|
|
||||||
"""
|
|
||||||
Low level WebSocket interface.
|
|
||||||
|
|
||||||
This class is based on the WebSocket protocol `draft-hixie-thewebsocketprotocol-76 <http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76>`_
|
|
||||||
|
|
||||||
We can connect to the websocket server and send/receive data.
|
|
||||||
The following example is an echo client.
|
|
||||||
|
|
||||||
>>> import websocket
|
|
||||||
>>> ws = websocket.WebSocket()
|
|
||||||
>>> ws.connect("ws://echo.websocket.events")
|
|
||||||
>>> ws.recv()
|
|
||||||
'echo.websocket.events sponsored by Lob.com'
|
|
||||||
>>> ws.send("Hello, Server")
|
|
||||||
19
|
|
||||||
>>> ws.recv()
|
|
||||||
'Hello, Server'
|
|
||||||
>>> ws.close()
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
get_mask_key: func
|
|
||||||
A callable function to get new mask keys, see the
|
|
||||||
WebSocket.set_mask_key's docstring for more information.
|
|
||||||
sockopt: tuple
|
|
||||||
Values for socket.setsockopt.
|
|
||||||
sockopt must be tuple and each element is argument of sock.setsockopt.
|
|
||||||
sslopt: dict
|
|
||||||
Optional dict object for ssl socket options. See FAQ for details.
|
|
||||||
fire_cont_frame: bool
|
|
||||||
Fire recv event for each cont frame. Default is False.
|
|
||||||
enable_multithread: bool
|
|
||||||
If set to True, lock send method.
|
|
||||||
skip_utf8_validation: bool
|
|
||||||
Skip utf8 validation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
get_mask_key=None,
|
|
||||||
sockopt=None,
|
|
||||||
sslopt=None,
|
|
||||||
fire_cont_frame: bool = False,
|
|
||||||
enable_multithread: bool = True,
|
|
||||||
skip_utf8_validation: bool = False,
|
|
||||||
**_,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Initialize WebSocket object.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
sslopt: dict
|
|
||||||
Optional dict object for ssl socket options. See FAQ for details.
|
|
||||||
"""
|
|
||||||
self.sock_opt = sock_opt(sockopt, sslopt)
|
|
||||||
self.handshake_response = None
|
|
||||||
self.sock: Optional[socket.socket] = None
|
|
||||||
|
|
||||||
self.connected = False
|
|
||||||
self.get_mask_key = get_mask_key
|
|
||||||
# These buffer over the build-up of a single frame.
|
|
||||||
self.frame_buffer = frame_buffer(self._recv, skip_utf8_validation)
|
|
||||||
self.cont_frame = continuous_frame(fire_cont_frame, skip_utf8_validation)
|
|
||||||
|
|
||||||
if enable_multithread:
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
self.readlock = threading.Lock()
|
|
||||||
else:
|
|
||||||
self.lock = NoLock()
|
|
||||||
self.readlock = NoLock()
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
"""
|
|
||||||
Allow iteration over websocket, implying sequential `recv` executions.
|
|
||||||
"""
|
|
||||||
while True:
|
|
||||||
yield self.recv()
|
|
||||||
|
|
||||||
def __next__(self):
|
|
||||||
return self.recv()
|
|
||||||
|
|
||||||
def next(self):
|
|
||||||
return self.__next__()
|
|
||||||
|
|
||||||
def fileno(self):
|
|
||||||
return self.sock.fileno()
|
|
||||||
|
|
||||||
def set_mask_key(self, func):
|
|
||||||
"""
|
|
||||||
Set function to create mask key. You can customize mask key generator.
|
|
||||||
Mainly, this is for testing purpose.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
func: func
|
|
||||||
callable object. the func takes 1 argument as integer.
|
|
||||||
The argument means length of mask key.
|
|
||||||
This func must return string(byte array),
|
|
||||||
which length is argument specified.
|
|
||||||
"""
|
|
||||||
self.get_mask_key = func
|
|
||||||
|
|
||||||
def gettimeout(self) -> Union[float, int, None]:
|
|
||||||
"""
|
|
||||||
Get the websocket timeout (in seconds) as an int or float
|
|
||||||
|
|
||||||
Returns
|
|
||||||
----------
|
|
||||||
timeout: int or float
|
|
||||||
returns timeout value (in seconds). This value could be either float/integer.
|
|
||||||
"""
|
|
||||||
return self.sock_opt.timeout
|
|
||||||
|
|
||||||
def settimeout(self, timeout: Union[float, int, None]):
|
|
||||||
"""
|
|
||||||
Set the timeout to the websocket.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
timeout: int or float
|
|
||||||
timeout time (in seconds). This value could be either float/integer.
|
|
||||||
"""
|
|
||||||
self.sock_opt.timeout = timeout
|
|
||||||
if self.sock:
|
|
||||||
self.sock.settimeout(timeout)
|
|
||||||
|
|
||||||
timeout = property(gettimeout, settimeout)
|
|
||||||
|
|
||||||
def getsubprotocol(self):
|
|
||||||
"""
|
|
||||||
Get subprotocol
|
|
||||||
"""
|
|
||||||
if self.handshake_response:
|
|
||||||
return self.handshake_response.subprotocol
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
subprotocol = property(getsubprotocol)
|
|
||||||
|
|
||||||
def getstatus(self):
|
|
||||||
"""
|
|
||||||
Get handshake status
|
|
||||||
"""
|
|
||||||
if self.handshake_response:
|
|
||||||
return self.handshake_response.status
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
status = property(getstatus)
|
|
||||||
|
|
||||||
def getheaders(self):
|
|
||||||
"""
|
|
||||||
Get handshake response header
|
|
||||||
"""
|
|
||||||
if self.handshake_response:
|
|
||||||
return self.handshake_response.headers
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def is_ssl(self):
|
|
||||||
try:
|
|
||||||
return isinstance(self.sock, ssl.SSLSocket)
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
headers = property(getheaders)
|
|
||||||
|
|
||||||
def connect(self, url, **options):
|
|
||||||
"""
|
|
||||||
Connect to url. url is websocket url scheme.
|
|
||||||
ie. ws://host:port/resource
|
|
||||||
You can customize using 'options'.
|
|
||||||
If you set "header" list object, you can set your own custom header.
|
|
||||||
|
|
||||||
>>> ws = WebSocket()
|
|
||||||
>>> ws.connect("ws://echo.websocket.events",
|
|
||||||
... header=["User-Agent: MyProgram",
|
|
||||||
... "x-custom: header"])
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
header: list or dict
|
|
||||||
Custom http header list or dict.
|
|
||||||
cookie: str
|
|
||||||
Cookie value.
|
|
||||||
origin: str
|
|
||||||
Custom origin url.
|
|
||||||
connection: str
|
|
||||||
Custom connection header value.
|
|
||||||
Default value "Upgrade" set in _handshake.py
|
|
||||||
suppress_origin: bool
|
|
||||||
Suppress outputting origin header.
|
|
||||||
host: str
|
|
||||||
Custom host header string.
|
|
||||||
timeout: int or float
|
|
||||||
Socket timeout time. This value is an integer or float.
|
|
||||||
If you set None for this value, it means "use default_timeout value"
|
|
||||||
http_proxy_host: str
|
|
||||||
HTTP proxy host name.
|
|
||||||
http_proxy_port: str or int
|
|
||||||
HTTP proxy port. Default is 80.
|
|
||||||
http_no_proxy: list
|
|
||||||
Whitelisted host names that don't use the proxy.
|
|
||||||
http_proxy_auth: tuple
|
|
||||||
HTTP proxy auth information. Tuple of username and password. Default is None.
|
|
||||||
http_proxy_timeout: int or float
|
|
||||||
HTTP proxy timeout, default is 60 sec as per python-socks.
|
|
||||||
redirect_limit: int
|
|
||||||
Number of redirects to follow.
|
|
||||||
subprotocols: list
|
|
||||||
List of available subprotocols. Default is None.
|
|
||||||
socket: socket
|
|
||||||
Pre-initialized stream socket.
|
|
||||||
"""
|
|
||||||
self.sock_opt.timeout = options.get("timeout", self.sock_opt.timeout)
|
|
||||||
self.sock, addrs = connect(
|
|
||||||
url, self.sock_opt, proxy_info(**options), options.pop("socket", None)
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.handshake_response = handshake(self.sock, url, *addrs, **options)
|
|
||||||
for _ in range(options.pop("redirect_limit", 3)):
|
|
||||||
if self.handshake_response.status in SUPPORTED_REDIRECT_STATUSES:
|
|
||||||
url = self.handshake_response.headers["location"]
|
|
||||||
self.sock.close()
|
|
||||||
self.sock, addrs = connect(
|
|
||||||
url,
|
|
||||||
self.sock_opt,
|
|
||||||
proxy_info(**options),
|
|
||||||
options.pop("socket", None),
|
|
||||||
)
|
|
||||||
self.handshake_response = handshake(
|
|
||||||
self.sock, url, *addrs, **options
|
|
||||||
)
|
|
||||||
self.connected = True
|
|
||||||
except:
|
|
||||||
if self.sock:
|
|
||||||
self.sock.close()
|
|
||||||
self.sock = None
|
|
||||||
raise
|
|
||||||
|
|
||||||
def send(self, payload: Union[bytes, str], opcode: int = ABNF.OPCODE_TEXT) -> int:
|
|
||||||
"""
|
|
||||||
Send the data as string.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
payload: str
|
|
||||||
Payload must be utf-8 string or unicode,
|
|
||||||
If the opcode is OPCODE_TEXT.
|
|
||||||
Otherwise, it must be string(byte array).
|
|
||||||
opcode: int
|
|
||||||
Operation code (opcode) to send.
|
|
||||||
"""
|
|
||||||
|
|
||||||
frame = ABNF.create_frame(payload, opcode)
|
|
||||||
return self.send_frame(frame)
|
|
||||||
|
|
||||||
def send_text(self, text_data: str) -> int:
|
|
||||||
"""
|
|
||||||
Sends UTF-8 encoded text.
|
|
||||||
"""
|
|
||||||
return self.send(text_data, ABNF.OPCODE_TEXT)
|
|
||||||
|
|
||||||
def send_bytes(self, data: Union[bytes, bytearray]) -> int:
|
|
||||||
"""
|
|
||||||
Sends a sequence of bytes.
|
|
||||||
"""
|
|
||||||
return self.send(data, ABNF.OPCODE_BINARY)
|
|
||||||
|
|
||||||
def send_frame(self, frame) -> int:
|
|
||||||
"""
|
|
||||||
Send the data frame.
|
|
||||||
|
|
||||||
>>> ws = create_connection("ws://echo.websocket.events")
|
|
||||||
>>> frame = ABNF.create_frame("Hello", ABNF.OPCODE_TEXT)
|
|
||||||
>>> ws.send_frame(frame)
|
|
||||||
>>> cont_frame = ABNF.create_frame("My name is ", ABNF.OPCODE_CONT, 0)
|
|
||||||
>>> ws.send_frame(frame)
|
|
||||||
>>> cont_frame = ABNF.create_frame("Foo Bar", ABNF.OPCODE_CONT, 1)
|
|
||||||
>>> ws.send_frame(frame)
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
frame: ABNF frame
|
|
||||||
frame data created by ABNF.create_frame
|
|
||||||
"""
|
|
||||||
if self.get_mask_key:
|
|
||||||
frame.get_mask_key = self.get_mask_key
|
|
||||||
data = frame.format()
|
|
||||||
length = len(data)
|
|
||||||
if isEnabledForTrace():
|
|
||||||
trace(f"++Sent raw: {repr(data)}")
|
|
||||||
trace(f"++Sent decoded: {frame.__str__()}")
|
|
||||||
with self.lock:
|
|
||||||
while data:
|
|
||||||
l = self._send(data)
|
|
||||||
data = data[l:]
|
|
||||||
|
|
||||||
return length
|
|
||||||
|
|
||||||
def send_binary(self, payload: bytes) -> int:
|
|
||||||
"""
|
|
||||||
Send a binary message (OPCODE_BINARY).
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
payload: bytes
|
|
||||||
payload of message to send.
|
|
||||||
"""
|
|
||||||
return self.send(payload, ABNF.OPCODE_BINARY)
|
|
||||||
|
|
||||||
def ping(self, payload: Union[str, bytes] = ""):
|
|
||||||
"""
|
|
||||||
Send ping data.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
payload: str
|
|
||||||
data payload to send server.
|
|
||||||
"""
|
|
||||||
if isinstance(payload, str):
|
|
||||||
payload = payload.encode("utf-8")
|
|
||||||
self.send(payload, ABNF.OPCODE_PING)
|
|
||||||
|
|
||||||
def pong(self, payload: Union[str, bytes] = ""):
|
|
||||||
"""
|
|
||||||
Send pong data.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
payload: str
|
|
||||||
data payload to send server.
|
|
||||||
"""
|
|
||||||
if isinstance(payload, str):
|
|
||||||
payload = payload.encode("utf-8")
|
|
||||||
self.send(payload, ABNF.OPCODE_PONG)
|
|
||||||
|
|
||||||
def recv(self) -> Union[str, bytes]:
|
|
||||||
"""
|
|
||||||
Receive string data(byte array) from the server.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
----------
|
|
||||||
data: string (byte array) value.
|
|
||||||
"""
|
|
||||||
with self.readlock:
|
|
||||||
opcode, data = self.recv_data()
|
|
||||||
if opcode == ABNF.OPCODE_TEXT:
|
|
||||||
data_received: Union[bytes, str] = data
|
|
||||||
if isinstance(data_received, bytes):
|
|
||||||
return data_received.decode("utf-8")
|
|
||||||
elif isinstance(data_received, str):
|
|
||||||
return data_received
|
|
||||||
elif opcode == ABNF.OPCODE_BINARY:
|
|
||||||
data_binary: bytes = data
|
|
||||||
return data_binary
|
|
||||||
else:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def recv_data(self, control_frame: bool = False) -> tuple:
|
|
||||||
"""
|
|
||||||
Receive data with operation code.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
control_frame: bool
|
|
||||||
a boolean flag indicating whether to return control frame
|
|
||||||
data, defaults to False
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
opcode, frame.data: tuple
|
|
||||||
tuple of operation code and string(byte array) value.
|
|
||||||
"""
|
|
||||||
opcode, frame = self.recv_data_frame(control_frame)
|
|
||||||
return opcode, frame.data
|
|
||||||
|
|
||||||
def recv_data_frame(self, control_frame: bool = False) -> tuple:
|
|
||||||
"""
|
|
||||||
Receive data with operation code.
|
|
||||||
|
|
||||||
If a valid ping message is received, a pong response is sent.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
control_frame: bool
|
|
||||||
a boolean flag indicating whether to return control frame
|
|
||||||
data, defaults to False
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
frame.opcode, frame: tuple
|
|
||||||
tuple of operation code and string(byte array) value.
|
|
||||||
"""
|
|
||||||
while True:
|
|
||||||
frame = self.recv_frame()
|
|
||||||
if isEnabledForTrace():
|
|
||||||
trace(f"++Rcv raw: {repr(frame.format())}")
|
|
||||||
trace(f"++Rcv decoded: {frame.__str__()}")
|
|
||||||
if not frame:
|
|
||||||
# handle error:
|
|
||||||
# 'NoneType' object has no attribute 'opcode'
|
|
||||||
raise WebSocketProtocolException(f"Not a valid frame {frame}")
|
|
||||||
elif frame.opcode in (
|
|
||||||
ABNF.OPCODE_TEXT,
|
|
||||||
ABNF.OPCODE_BINARY,
|
|
||||||
ABNF.OPCODE_CONT,
|
|
||||||
):
|
|
||||||
self.cont_frame.validate(frame)
|
|
||||||
self.cont_frame.add(frame)
|
|
||||||
|
|
||||||
if self.cont_frame.is_fire(frame):
|
|
||||||
return self.cont_frame.extract(frame)
|
|
||||||
|
|
||||||
elif frame.opcode == ABNF.OPCODE_CLOSE:
|
|
||||||
self.send_close()
|
|
||||||
return frame.opcode, frame
|
|
||||||
elif frame.opcode == ABNF.OPCODE_PING:
|
|
||||||
if len(frame.data) < 126:
|
|
||||||
self.pong(frame.data)
|
|
||||||
else:
|
|
||||||
raise WebSocketProtocolException("Ping message is too long")
|
|
||||||
if control_frame:
|
|
||||||
return frame.opcode, frame
|
|
||||||
elif frame.opcode == ABNF.OPCODE_PONG:
|
|
||||||
if control_frame:
|
|
||||||
return frame.opcode, frame
|
|
||||||
|
|
||||||
def recv_frame(self):
|
|
||||||
"""
|
|
||||||
Receive data as frame from server.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
self.frame_buffer.recv_frame(): ABNF frame object
|
|
||||||
"""
|
|
||||||
return self.frame_buffer.recv_frame()
|
|
||||||
|
|
||||||
def send_close(self, status: int = STATUS_NORMAL, reason: bytes = b""):
|
|
||||||
"""
|
|
||||||
Send close data to the server.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
status: int
|
|
||||||
Status code to send. See STATUS_XXX.
|
|
||||||
reason: str or bytes
|
|
||||||
The reason to close. This must be string or UTF-8 bytes.
|
|
||||||
"""
|
|
||||||
if status < 0 or status >= ABNF.LENGTH_16:
|
|
||||||
raise ValueError("code is invalid range")
|
|
||||||
self.connected = False
|
|
||||||
self.send(struct.pack("!H", status) + reason, ABNF.OPCODE_CLOSE)
|
|
||||||
|
|
||||||
def close(self, status: int = STATUS_NORMAL, reason: bytes = b"", timeout: int = 3):
|
|
||||||
"""
|
|
||||||
Close Websocket object
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
status: int
|
|
||||||
Status code to send. See VALID_CLOSE_STATUS in ABNF.
|
|
||||||
reason: bytes
|
|
||||||
The reason to close in UTF-8.
|
|
||||||
timeout: int or float
|
|
||||||
Timeout until receive a close frame.
|
|
||||||
If None, it will wait forever until receive a close frame.
|
|
||||||
"""
|
|
||||||
if not self.connected:
|
|
||||||
return
|
|
||||||
if status < 0 or status >= ABNF.LENGTH_16:
|
|
||||||
raise ValueError("code is invalid range")
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.connected = False
|
|
||||||
self.send(struct.pack("!H", status) + reason, ABNF.OPCODE_CLOSE)
|
|
||||||
sock_timeout = self.sock.gettimeout()
|
|
||||||
self.sock.settimeout(timeout)
|
|
||||||
start_time = time.time()
|
|
||||||
while timeout is None or time.time() - start_time < timeout:
|
|
||||||
try:
|
|
||||||
frame = self.recv_frame()
|
|
||||||
if frame.opcode != ABNF.OPCODE_CLOSE:
|
|
||||||
continue
|
|
||||||
if isEnabledForError():
|
|
||||||
recv_status = struct.unpack("!H", frame.data[0:2])[0]
|
|
||||||
if recv_status >= 3000 and recv_status <= 4999:
|
|
||||||
debug(f"close status: {repr(recv_status)}")
|
|
||||||
elif recv_status != STATUS_NORMAL:
|
|
||||||
error(f"close status: {repr(recv_status)}")
|
|
||||||
break
|
|
||||||
except:
|
|
||||||
break
|
|
||||||
self.sock.settimeout(sock_timeout)
|
|
||||||
self.sock.shutdown(socket.SHUT_RDWR)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.shutdown()
|
|
||||||
|
|
||||||
def abort(self):
|
|
||||||
"""
|
|
||||||
Low-level asynchronous abort, wakes up other threads that are waiting in recv_*
|
|
||||||
"""
|
|
||||||
if self.connected:
|
|
||||||
self.sock.shutdown(socket.SHUT_RDWR)
|
|
||||||
|
|
||||||
def shutdown(self):
|
|
||||||
"""
|
|
||||||
close socket, immediately.
|
|
||||||
"""
|
|
||||||
if self.sock:
|
|
||||||
self.sock.close()
|
|
||||||
self.sock = None
|
|
||||||
self.connected = False
|
|
||||||
|
|
||||||
def _send(self, data: Union[str, bytes]):
|
|
||||||
return send(self.sock, data)
|
|
||||||
|
|
||||||
def _recv(self, bufsize):
|
|
||||||
try:
|
|
||||||
return recv(self.sock, bufsize)
|
|
||||||
except WebSocketConnectionClosedException:
|
|
||||||
if self.sock:
|
|
||||||
self.sock.close()
|
|
||||||
self.sock = None
|
|
||||||
self.connected = False
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
def create_connection(url: str, timeout=None, class_=WebSocket, **options):
|
|
||||||
"""
|
|
||||||
Connect to url and return websocket object.
|
|
||||||
|
|
||||||
Connect to url and return the WebSocket object.
|
|
||||||
Passing optional timeout parameter will set the timeout on the socket.
|
|
||||||
If no timeout is supplied,
|
|
||||||
the global default timeout setting returned by getdefaulttimeout() is used.
|
|
||||||
You can customize using 'options'.
|
|
||||||
If you set "header" list object, you can set your own custom header.
|
|
||||||
|
|
||||||
>>> conn = create_connection("ws://echo.websocket.events",
|
|
||||||
... header=["User-Agent: MyProgram",
|
|
||||||
... "x-custom: header"])
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
class_: class
|
|
||||||
class to instantiate when creating the connection. It has to implement
|
|
||||||
settimeout and connect. It's __init__ should be compatible with
|
|
||||||
WebSocket.__init__, i.e. accept all of it's kwargs.
|
|
||||||
header: list or dict
|
|
||||||
custom http header list or dict.
|
|
||||||
cookie: str
|
|
||||||
Cookie value.
|
|
||||||
origin: str
|
|
||||||
custom origin url.
|
|
||||||
suppress_origin: bool
|
|
||||||
suppress outputting origin header.
|
|
||||||
host: str
|
|
||||||
custom host header string.
|
|
||||||
timeout: int or float
|
|
||||||
socket timeout time. This value could be either float/integer.
|
|
||||||
If set to None, it uses the default_timeout value.
|
|
||||||
http_proxy_host: str
|
|
||||||
HTTP proxy host name.
|
|
||||||
http_proxy_port: str or int
|
|
||||||
HTTP proxy port. If not set, set to 80.
|
|
||||||
http_no_proxy: list
|
|
||||||
Whitelisted host names that don't use the proxy.
|
|
||||||
http_proxy_auth: tuple
|
|
||||||
HTTP proxy auth information. tuple of username and password. Default is None.
|
|
||||||
http_proxy_timeout: int or float
|
|
||||||
HTTP proxy timeout, default is 60 sec as per python-socks.
|
|
||||||
enable_multithread: bool
|
|
||||||
Enable lock for multithread.
|
|
||||||
redirect_limit: int
|
|
||||||
Number of redirects to follow.
|
|
||||||
sockopt: tuple
|
|
||||||
Values for socket.setsockopt.
|
|
||||||
sockopt must be a tuple and each element is an argument of sock.setsockopt.
|
|
||||||
sslopt: dict
|
|
||||||
Optional dict object for ssl socket options. See FAQ for details.
|
|
||||||
subprotocols: list
|
|
||||||
List of available subprotocols. Default is None.
|
|
||||||
skip_utf8_validation: bool
|
|
||||||
Skip utf8 validation.
|
|
||||||
socket: socket
|
|
||||||
Pre-initialized stream socket.
|
|
||||||
"""
|
|
||||||
sockopt = options.pop("sockopt", [])
|
|
||||||
sslopt = options.pop("sslopt", {})
|
|
||||||
fire_cont_frame = options.pop("fire_cont_frame", False)
|
|
||||||
enable_multithread = options.pop("enable_multithread", True)
|
|
||||||
skip_utf8_validation = options.pop("skip_utf8_validation", False)
|
|
||||||
websock = class_(
|
|
||||||
sockopt=sockopt,
|
|
||||||
sslopt=sslopt,
|
|
||||||
fire_cont_frame=fire_cont_frame,
|
|
||||||
enable_multithread=enable_multithread,
|
|
||||||
skip_utf8_validation=skip_utf8_validation,
|
|
||||||
**options,
|
|
||||||
)
|
|
||||||
websock.settimeout(timeout if timeout is not None else getdefaulttimeout())
|
|
||||||
websock.connect(url, **options)
|
|
||||||
return websock
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
"""
|
|
||||||
_exceptions.py
|
|
||||||
websocket - WebSocket client library for Python
|
|
||||||
|
|
||||||
Copyright 2024 engn33r
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketException(Exception):
|
|
||||||
"""
|
|
||||||
WebSocket exception class.
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketProtocolException(WebSocketException):
|
|
||||||
"""
|
|
||||||
If the WebSocket protocol is invalid, this exception will be raised.
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketPayloadException(WebSocketException):
|
|
||||||
"""
|
|
||||||
If the WebSocket payload is invalid, this exception will be raised.
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketConnectionClosedException(WebSocketException):
|
|
||||||
"""
|
|
||||||
If remote host closed the connection or some network error happened,
|
|
||||||
this exception will be raised.
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketTimeoutException(WebSocketException):
|
|
||||||
"""
|
|
||||||
WebSocketTimeoutException will be raised at socket timeout during read/write data.
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketProxyException(WebSocketException):
|
|
||||||
"""
|
|
||||||
WebSocketProxyException will be raised when proxy error occurred.
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketBadStatusException(WebSocketException):
|
|
||||||
"""
|
|
||||||
WebSocketBadStatusException will be raised when we get bad handshake status code.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
message: str,
|
|
||||||
status_code: int,
|
|
||||||
status_message=None,
|
|
||||||
resp_headers=None,
|
|
||||||
resp_body=None,
|
|
||||||
):
|
|
||||||
super().__init__(message)
|
|
||||||
self.status_code = status_code
|
|
||||||
self.resp_headers = resp_headers
|
|
||||||
self.resp_body = resp_body
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketAddressException(WebSocketException):
|
|
||||||
"""
|
|
||||||
If the websocket address info cannot be found, this exception will be raised.
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
"""
|
|
||||||
_handshake.py
|
|
||||||
websocket - WebSocket client library for Python
|
|
||||||
|
|
||||||
Copyright 2024 engn33r
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import hmac
|
|
||||||
import os
|
|
||||||
from base64 import encodebytes as base64encode
|
|
||||||
from http import HTTPStatus
|
|
||||||
|
|
||||||
from ._cookiejar import SimpleCookieJar
|
|
||||||
from ._exceptions import WebSocketException, WebSocketBadStatusException
|
|
||||||
from ._http import read_headers
|
|
||||||
from ._logging import dump, error
|
|
||||||
from ._socket import send
|
|
||||||
|
|
||||||
__all__ = ["handshake_response", "handshake", "SUPPORTED_REDIRECT_STATUSES"]
|
|
||||||
|
|
||||||
# websocket supported version.
|
|
||||||
VERSION = 13
|
|
||||||
|
|
||||||
SUPPORTED_REDIRECT_STATUSES = (
|
|
||||||
HTTPStatus.MOVED_PERMANENTLY,
|
|
||||||
HTTPStatus.FOUND,
|
|
||||||
HTTPStatus.SEE_OTHER,
|
|
||||||
HTTPStatus.TEMPORARY_REDIRECT,
|
|
||||||
HTTPStatus.PERMANENT_REDIRECT,
|
|
||||||
)
|
|
||||||
SUCCESS_STATUSES = SUPPORTED_REDIRECT_STATUSES + (HTTPStatus.SWITCHING_PROTOCOLS,)
|
|
||||||
|
|
||||||
CookieJar = SimpleCookieJar()
|
|
||||||
|
|
||||||
|
|
||||||
class handshake_response:
|
|
||||||
def __init__(self, status: int, headers: dict, subprotocol):
|
|
||||||
self.status = status
|
|
||||||
self.headers = headers
|
|
||||||
self.subprotocol = subprotocol
|
|
||||||
CookieJar.add(headers.get("set-cookie"))
|
|
||||||
|
|
||||||
|
|
||||||
def handshake(
|
|
||||||
sock, url: str, hostname: str, port: int, resource: str, **options
|
|
||||||
) -> handshake_response:
|
|
||||||
headers, key = _get_handshake_headers(resource, url, hostname, port, options)
|
|
||||||
|
|
||||||
header_str = "\r\n".join(headers)
|
|
||||||
send(sock, header_str)
|
|
||||||
dump("request header", header_str)
|
|
||||||
|
|
||||||
status, resp = _get_resp_headers(sock)
|
|
||||||
if status in SUPPORTED_REDIRECT_STATUSES:
|
|
||||||
return handshake_response(status, resp, None)
|
|
||||||
success, subproto = _validate(resp, key, options.get("subprotocols"))
|
|
||||||
if not success:
|
|
||||||
raise WebSocketException("Invalid WebSocket Header")
|
|
||||||
|
|
||||||
return handshake_response(status, resp, subproto)
|
|
||||||
|
|
||||||
|
|
||||||
def _pack_hostname(hostname: str) -> str:
|
|
||||||
# IPv6 address
|
|
||||||
if ":" in hostname:
|
|
||||||
return f"[{hostname}]"
|
|
||||||
return hostname
|
|
||||||
|
|
||||||
|
|
||||||
def _get_handshake_headers(
|
|
||||||
resource: str, url: str, host: str, port: int, options: dict
|
|
||||||
) -> tuple:
|
|
||||||
headers = [f"GET {resource} HTTP/1.1", "Upgrade: websocket"]
|
|
||||||
if port in [80, 443]:
|
|
||||||
hostport = _pack_hostname(host)
|
|
||||||
else:
|
|
||||||
hostport = f"{_pack_hostname(host)}:{port}"
|
|
||||||
if options.get("host"):
|
|
||||||
headers.append(f'Host: {options["host"]}')
|
|
||||||
else:
|
|
||||||
headers.append(f"Host: {hostport}")
|
|
||||||
|
|
||||||
# scheme indicates whether http or https is used in Origin
|
|
||||||
# The same approach is used in parse_url of _url.py to set default port
|
|
||||||
scheme, url = url.split(":", 1)
|
|
||||||
if not options.get("suppress_origin"):
|
|
||||||
if "origin" in options and options["origin"] is not None:
|
|
||||||
headers.append(f'Origin: {options["origin"]}')
|
|
||||||
elif scheme == "wss":
|
|
||||||
headers.append(f"Origin: https://{hostport}")
|
|
||||||
else:
|
|
||||||
headers.append(f"Origin: http://{hostport}")
|
|
||||||
|
|
||||||
key = _create_sec_websocket_key()
|
|
||||||
|
|
||||||
# Append Sec-WebSocket-Key & Sec-WebSocket-Version if not manually specified
|
|
||||||
if not options.get("header") or "Sec-WebSocket-Key" not in options["header"]:
|
|
||||||
headers.append(f"Sec-WebSocket-Key: {key}")
|
|
||||||
else:
|
|
||||||
key = options["header"]["Sec-WebSocket-Key"]
|
|
||||||
|
|
||||||
if not options.get("header") or "Sec-WebSocket-Version" not in options["header"]:
|
|
||||||
headers.append(f"Sec-WebSocket-Version: {VERSION}")
|
|
||||||
|
|
||||||
if not options.get("connection"):
|
|
||||||
headers.append("Connection: Upgrade")
|
|
||||||
else:
|
|
||||||
headers.append(options["connection"])
|
|
||||||
|
|
||||||
if subprotocols := options.get("subprotocols"):
|
|
||||||
headers.append(f'Sec-WebSocket-Protocol: {",".join(subprotocols)}')
|
|
||||||
|
|
||||||
if header := options.get("header"):
|
|
||||||
if isinstance(header, dict):
|
|
||||||
header = [": ".join([k, v]) for k, v in header.items() if v is not None]
|
|
||||||
headers.extend(header)
|
|
||||||
|
|
||||||
server_cookie = CookieJar.get(host)
|
|
||||||
client_cookie = options.get("cookie", None)
|
|
||||||
|
|
||||||
if cookie := "; ".join(filter(None, [server_cookie, client_cookie])):
|
|
||||||
headers.append(f"Cookie: {cookie}")
|
|
||||||
|
|
||||||
headers.extend(("", ""))
|
|
||||||
return headers, key
|
|
||||||
|
|
||||||
|
|
||||||
def _get_resp_headers(sock, success_statuses: tuple = SUCCESS_STATUSES) -> tuple:
|
|
||||||
status, resp_headers, status_message = read_headers(sock)
|
|
||||||
if status not in success_statuses:
|
|
||||||
content_len = resp_headers.get("content-length")
|
|
||||||
if content_len:
|
|
||||||
response_body = sock.recv(
|
|
||||||
int(content_len)
|
|
||||||
) # read the body of the HTTP error message response and include it in the exception
|
|
||||||
else:
|
|
||||||
response_body = None
|
|
||||||
raise WebSocketBadStatusException(
|
|
||||||
f"Handshake status {status} {status_message} -+-+- {resp_headers} -+-+- {response_body}",
|
|
||||||
status,
|
|
||||||
status_message,
|
|
||||||
resp_headers,
|
|
||||||
response_body,
|
|
||||||
)
|
|
||||||
return status, resp_headers
|
|
||||||
|
|
||||||
|
|
||||||
_HEADERS_TO_CHECK = {
|
|
||||||
"upgrade": "websocket",
|
|
||||||
"connection": "upgrade",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _validate(headers, key: str, subprotocols) -> tuple:
|
|
||||||
subproto = None
|
|
||||||
for k, v in _HEADERS_TO_CHECK.items():
|
|
||||||
r = headers.get(k, None)
|
|
||||||
if not r:
|
|
||||||
return False, None
|
|
||||||
r = [x.strip().lower() for x in r.split(",")]
|
|
||||||
if v not in r:
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
if subprotocols:
|
|
||||||
subproto = headers.get("sec-websocket-protocol", None)
|
|
||||||
if not subproto or subproto.lower() not in [s.lower() for s in subprotocols]:
|
|
||||||
error(f"Invalid subprotocol: {subprotocols}")
|
|
||||||
return False, None
|
|
||||||
subproto = subproto.lower()
|
|
||||||
|
|
||||||
result = headers.get("sec-websocket-accept", None)
|
|
||||||
if not result:
|
|
||||||
return False, None
|
|
||||||
result = result.lower()
|
|
||||||
|
|
||||||
if isinstance(result, str):
|
|
||||||
result = result.encode("utf-8")
|
|
||||||
|
|
||||||
value = f"{key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11".encode("utf-8")
|
|
||||||
hashed = base64encode(hashlib.sha1(value).digest()).strip().lower()
|
|
||||||
|
|
||||||
if hmac.compare_digest(hashed, result):
|
|
||||||
return True, subproto
|
|
||||||
else:
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
|
|
||||||
def _create_sec_websocket_key() -> str:
|
|
||||||
randomness = os.urandom(16)
|
|
||||||
return base64encode(randomness).decode("utf-8").strip()
|
|
||||||
@@ -1,374 +0,0 @@
|
|||||||
"""
|
|
||||||
_http.py
|
|
||||||
websocket - WebSocket client library for Python
|
|
||||||
|
|
||||||
Copyright 2024 engn33r
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import errno
|
|
||||||
import os
|
|
||||||
import socket
|
|
||||||
from base64 import encodebytes as base64encode
|
|
||||||
|
|
||||||
from ._exceptions import (
|
|
||||||
WebSocketAddressException,
|
|
||||||
WebSocketException,
|
|
||||||
WebSocketProxyException,
|
|
||||||
)
|
|
||||||
from ._logging import debug, dump, trace
|
|
||||||
from ._socket import DEFAULT_SOCKET_OPTION, recv_line, send
|
|
||||||
from ._ssl_compat import HAVE_SSL, ssl
|
|
||||||
from ._url import get_proxy_info, parse_url
|
|
||||||
|
|
||||||
__all__ = ["proxy_info", "connect", "read_headers"]
|
|
||||||
|
|
||||||
try:
|
|
||||||
from python_socks._errors import *
|
|
||||||
from python_socks._types import ProxyType
|
|
||||||
from python_socks.sync import Proxy
|
|
||||||
|
|
||||||
HAVE_PYTHON_SOCKS = True
|
|
||||||
except:
|
|
||||||
HAVE_PYTHON_SOCKS = False
|
|
||||||
|
|
||||||
class ProxyError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ProxyTimeoutError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ProxyConnectionError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class proxy_info:
|
|
||||||
def __init__(self, **options):
|
|
||||||
self.proxy_host = options.get("http_proxy_host", None)
|
|
||||||
if self.proxy_host:
|
|
||||||
self.proxy_port = options.get("http_proxy_port", 0)
|
|
||||||
self.auth = options.get("http_proxy_auth", None)
|
|
||||||
self.no_proxy = options.get("http_no_proxy", None)
|
|
||||||
self.proxy_protocol = options.get("proxy_type", "http")
|
|
||||||
# Note: If timeout not specified, default python-socks timeout is 60 seconds
|
|
||||||
self.proxy_timeout = options.get("http_proxy_timeout", None)
|
|
||||||
if self.proxy_protocol not in [
|
|
||||||
"http",
|
|
||||||
"socks4",
|
|
||||||
"socks4a",
|
|
||||||
"socks5",
|
|
||||||
"socks5h",
|
|
||||||
]:
|
|
||||||
raise ProxyError(
|
|
||||||
"Only http, socks4, socks5 proxy protocols are supported"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.proxy_port = 0
|
|
||||||
self.auth = None
|
|
||||||
self.no_proxy = None
|
|
||||||
self.proxy_protocol = "http"
|
|
||||||
|
|
||||||
|
|
||||||
def _start_proxied_socket(url: str, options, proxy) -> tuple:
|
|
||||||
if not HAVE_PYTHON_SOCKS:
|
|
||||||
raise WebSocketException(
|
|
||||||
"Python Socks is needed for SOCKS proxying but is not available"
|
|
||||||
)
|
|
||||||
|
|
||||||
hostname, port, resource, is_secure = parse_url(url)
|
|
||||||
|
|
||||||
if proxy.proxy_protocol == "socks4":
|
|
||||||
rdns = False
|
|
||||||
proxy_type = ProxyType.SOCKS4
|
|
||||||
# socks4a sends DNS through proxy
|
|
||||||
elif proxy.proxy_protocol == "socks4a":
|
|
||||||
rdns = True
|
|
||||||
proxy_type = ProxyType.SOCKS4
|
|
||||||
elif proxy.proxy_protocol == "socks5":
|
|
||||||
rdns = False
|
|
||||||
proxy_type = ProxyType.SOCKS5
|
|
||||||
# socks5h sends DNS through proxy
|
|
||||||
elif proxy.proxy_protocol == "socks5h":
|
|
||||||
rdns = True
|
|
||||||
proxy_type = ProxyType.SOCKS5
|
|
||||||
|
|
||||||
ws_proxy = Proxy.create(
|
|
||||||
proxy_type=proxy_type,
|
|
||||||
host=proxy.proxy_host,
|
|
||||||
port=int(proxy.proxy_port),
|
|
||||||
username=proxy.auth[0] if proxy.auth else None,
|
|
||||||
password=proxy.auth[1] if proxy.auth else None,
|
|
||||||
rdns=rdns,
|
|
||||||
)
|
|
||||||
|
|
||||||
sock = ws_proxy.connect(hostname, port, timeout=proxy.proxy_timeout)
|
|
||||||
|
|
||||||
if is_secure:
|
|
||||||
if HAVE_SSL:
|
|
||||||
sock = _ssl_socket(sock, options.sslopt, hostname)
|
|
||||||
else:
|
|
||||||
raise WebSocketException("SSL not available.")
|
|
||||||
|
|
||||||
return sock, (hostname, port, resource)
|
|
||||||
|
|
||||||
|
|
||||||
def connect(url: str, options, proxy, socket):
|
|
||||||
# Use _start_proxied_socket() only for socks4 or socks5 proxy
|
|
||||||
# Use _tunnel() for http proxy
|
|
||||||
# TODO: Use python-socks for http protocol also, to standardize flow
|
|
||||||
if proxy.proxy_host and not socket and proxy.proxy_protocol != "http":
|
|
||||||
return _start_proxied_socket(url, options, proxy)
|
|
||||||
|
|
||||||
hostname, port_from_url, resource, is_secure = parse_url(url)
|
|
||||||
|
|
||||||
if socket:
|
|
||||||
return socket, (hostname, port_from_url, resource)
|
|
||||||
|
|
||||||
addrinfo_list, need_tunnel, auth = _get_addrinfo_list(
|
|
||||||
hostname, port_from_url, is_secure, proxy
|
|
||||||
)
|
|
||||||
if not addrinfo_list:
|
|
||||||
raise WebSocketException(f"Host not found.: {hostname}:{port_from_url}")
|
|
||||||
|
|
||||||
sock = None
|
|
||||||
try:
|
|
||||||
sock = _open_socket(addrinfo_list, options.sockopt, options.timeout)
|
|
||||||
if need_tunnel:
|
|
||||||
sock = _tunnel(sock, hostname, port_from_url, auth)
|
|
||||||
|
|
||||||
if is_secure:
|
|
||||||
if HAVE_SSL:
|
|
||||||
sock = _ssl_socket(sock, options.sslopt, hostname)
|
|
||||||
else:
|
|
||||||
raise WebSocketException("SSL not available.")
|
|
||||||
|
|
||||||
return sock, (hostname, port_from_url, resource)
|
|
||||||
except:
|
|
||||||
if sock:
|
|
||||||
sock.close()
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
def _get_addrinfo_list(hostname, port: int, is_secure: bool, proxy) -> tuple:
|
|
||||||
phost, pport, pauth = get_proxy_info(
|
|
||||||
hostname,
|
|
||||||
is_secure,
|
|
||||||
proxy.proxy_host,
|
|
||||||
proxy.proxy_port,
|
|
||||||
proxy.auth,
|
|
||||||
proxy.no_proxy,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
# when running on windows 10, getaddrinfo without socktype returns a socktype 0.
|
|
||||||
# This generates an error exception: `_on_error: exception Socket type must be stream or datagram, not 0`
|
|
||||||
# or `OSError: [Errno 22] Invalid argument` when creating socket. Force the socket type to SOCK_STREAM.
|
|
||||||
if not phost:
|
|
||||||
addrinfo_list = socket.getaddrinfo(
|
|
||||||
hostname, port, 0, socket.SOCK_STREAM, socket.SOL_TCP
|
|
||||||
)
|
|
||||||
return addrinfo_list, False, None
|
|
||||||
else:
|
|
||||||
pport = pport and pport or 80
|
|
||||||
# when running on windows 10, the getaddrinfo used above
|
|
||||||
# returns a socktype 0. This generates an error exception:
|
|
||||||
# _on_error: exception Socket type must be stream or datagram, not 0
|
|
||||||
# Force the socket type to SOCK_STREAM
|
|
||||||
addrinfo_list = socket.getaddrinfo(
|
|
||||||
phost, pport, 0, socket.SOCK_STREAM, socket.SOL_TCP
|
|
||||||
)
|
|
||||||
return addrinfo_list, True, pauth
|
|
||||||
except socket.gaierror as e:
|
|
||||||
raise WebSocketAddressException(e)
|
|
||||||
|
|
||||||
|
|
||||||
def _open_socket(addrinfo_list, sockopt, timeout):
|
|
||||||
err = None
|
|
||||||
for addrinfo in addrinfo_list:
|
|
||||||
family, socktype, proto = addrinfo[:3]
|
|
||||||
sock = socket.socket(family, socktype, proto)
|
|
||||||
sock.settimeout(timeout)
|
|
||||||
for opts in DEFAULT_SOCKET_OPTION:
|
|
||||||
sock.setsockopt(*opts)
|
|
||||||
for opts in sockopt:
|
|
||||||
sock.setsockopt(*opts)
|
|
||||||
|
|
||||||
address = addrinfo[4]
|
|
||||||
err = None
|
|
||||||
while not err:
|
|
||||||
try:
|
|
||||||
sock.connect(address)
|
|
||||||
except socket.error as error:
|
|
||||||
sock.close()
|
|
||||||
error.remote_ip = str(address[0])
|
|
||||||
try:
|
|
||||||
eConnRefused = (
|
|
||||||
errno.ECONNREFUSED,
|
|
||||||
errno.WSAECONNREFUSED,
|
|
||||||
errno.ENETUNREACH,
|
|
||||||
)
|
|
||||||
except AttributeError:
|
|
||||||
eConnRefused = (errno.ECONNREFUSED, errno.ENETUNREACH)
|
|
||||||
if error.errno not in eConnRefused:
|
|
||||||
raise error
|
|
||||||
err = error
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
if err:
|
|
||||||
raise err
|
|
||||||
|
|
||||||
return sock
|
|
||||||
|
|
||||||
|
|
||||||
def _wrap_sni_socket(sock: socket.socket, sslopt: dict, hostname, check_hostname):
|
|
||||||
context = sslopt.get("context", None)
|
|
||||||
if not context:
|
|
||||||
context = ssl.SSLContext(sslopt.get("ssl_version", ssl.PROTOCOL_TLS_CLIENT))
|
|
||||||
# Non default context need to manually enable SSLKEYLOGFILE support by setting the keylog_filename attribute.
|
|
||||||
# For more details see also:
|
|
||||||
# * https://docs.python.org/3.8/library/ssl.html?highlight=sslkeylogfile#context-creation
|
|
||||||
# * https://docs.python.org/3.8/library/ssl.html?highlight=sslkeylogfile#ssl.SSLContext.keylog_filename
|
|
||||||
context.keylog_filename = os.environ.get("SSLKEYLOGFILE", None)
|
|
||||||
|
|
||||||
if sslopt.get("cert_reqs", ssl.CERT_NONE) != ssl.CERT_NONE:
|
|
||||||
cafile = sslopt.get("ca_certs", None)
|
|
||||||
capath = sslopt.get("ca_cert_path", None)
|
|
||||||
if cafile or capath:
|
|
||||||
context.load_verify_locations(cafile=cafile, capath=capath)
|
|
||||||
elif hasattr(context, "load_default_certs"):
|
|
||||||
context.load_default_certs(ssl.Purpose.SERVER_AUTH)
|
|
||||||
if sslopt.get("certfile", None):
|
|
||||||
context.load_cert_chain(
|
|
||||||
sslopt["certfile"],
|
|
||||||
sslopt.get("keyfile", None),
|
|
||||||
sslopt.get("password", None),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Python 3.10 switch to PROTOCOL_TLS_CLIENT defaults to "cert_reqs = ssl.CERT_REQUIRED" and "check_hostname = True"
|
|
||||||
# If both disabled, set check_hostname before verify_mode
|
|
||||||
# see https://github.com/liris/websocket-client/commit/b96a2e8fa765753e82eea531adb19716b52ca3ca#commitcomment-10803153
|
|
||||||
if sslopt.get("cert_reqs", ssl.CERT_NONE) == ssl.CERT_NONE and not sslopt.get(
|
|
||||||
"check_hostname", False
|
|
||||||
):
|
|
||||||
context.check_hostname = False
|
|
||||||
context.verify_mode = ssl.CERT_NONE
|
|
||||||
else:
|
|
||||||
context.check_hostname = sslopt.get("check_hostname", True)
|
|
||||||
context.verify_mode = sslopt.get("cert_reqs", ssl.CERT_REQUIRED)
|
|
||||||
|
|
||||||
if "ciphers" in sslopt:
|
|
||||||
context.set_ciphers(sslopt["ciphers"])
|
|
||||||
if "cert_chain" in sslopt:
|
|
||||||
certfile, keyfile, password = sslopt["cert_chain"]
|
|
||||||
context.load_cert_chain(certfile, keyfile, password)
|
|
||||||
if "ecdh_curve" in sslopt:
|
|
||||||
context.set_ecdh_curve(sslopt["ecdh_curve"])
|
|
||||||
|
|
||||||
return context.wrap_socket(
|
|
||||||
sock,
|
|
||||||
do_handshake_on_connect=sslopt.get("do_handshake_on_connect", True),
|
|
||||||
suppress_ragged_eofs=sslopt.get("suppress_ragged_eofs", True),
|
|
||||||
server_hostname=hostname,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _ssl_socket(sock: socket.socket, user_sslopt: dict, hostname):
|
|
||||||
sslopt: dict = {"cert_reqs": ssl.CERT_REQUIRED}
|
|
||||||
sslopt.update(user_sslopt)
|
|
||||||
|
|
||||||
cert_path = os.environ.get("WEBSOCKET_CLIENT_CA_BUNDLE")
|
|
||||||
if (
|
|
||||||
cert_path
|
|
||||||
and os.path.isfile(cert_path)
|
|
||||||
and user_sslopt.get("ca_certs", None) is None
|
|
||||||
):
|
|
||||||
sslopt["ca_certs"] = cert_path
|
|
||||||
elif (
|
|
||||||
cert_path
|
|
||||||
and os.path.isdir(cert_path)
|
|
||||||
and user_sslopt.get("ca_cert_path", None) is None
|
|
||||||
):
|
|
||||||
sslopt["ca_cert_path"] = cert_path
|
|
||||||
|
|
||||||
if sslopt.get("server_hostname", None):
|
|
||||||
hostname = sslopt["server_hostname"]
|
|
||||||
|
|
||||||
check_hostname = sslopt.get("check_hostname", True)
|
|
||||||
sock = _wrap_sni_socket(sock, sslopt, hostname, check_hostname)
|
|
||||||
|
|
||||||
return sock
|
|
||||||
|
|
||||||
|
|
||||||
def _tunnel(sock: socket.socket, host, port: int, auth) -> socket.socket:
|
|
||||||
debug("Connecting proxy...")
|
|
||||||
connect_header = f"CONNECT {host}:{port} HTTP/1.1\r\n"
|
|
||||||
connect_header += f"Host: {host}:{port}\r\n"
|
|
||||||
|
|
||||||
# TODO: support digest auth.
|
|
||||||
if auth and auth[0]:
|
|
||||||
auth_str = auth[0]
|
|
||||||
if auth[1]:
|
|
||||||
auth_str += f":{auth[1]}"
|
|
||||||
encoded_str = base64encode(auth_str.encode()).strip().decode().replace("\n", "")
|
|
||||||
connect_header += f"Proxy-Authorization: Basic {encoded_str}\r\n"
|
|
||||||
connect_header += "\r\n"
|
|
||||||
dump("request header", connect_header)
|
|
||||||
|
|
||||||
send(sock, connect_header)
|
|
||||||
|
|
||||||
try:
|
|
||||||
status, _, _ = read_headers(sock)
|
|
||||||
except Exception as e:
|
|
||||||
raise WebSocketProxyException(str(e))
|
|
||||||
|
|
||||||
if status != 200:
|
|
||||||
raise WebSocketProxyException(f"failed CONNECT via proxy status: {status}")
|
|
||||||
|
|
||||||
return sock
|
|
||||||
|
|
||||||
|
|
||||||
def read_headers(sock: socket.socket) -> tuple:
|
|
||||||
status = None
|
|
||||||
status_message = None
|
|
||||||
headers: dict = {}
|
|
||||||
trace("--- response header ---")
|
|
||||||
|
|
||||||
while True:
|
|
||||||
line = recv_line(sock)
|
|
||||||
line = line.decode("utf-8").strip()
|
|
||||||
if not line:
|
|
||||||
break
|
|
||||||
trace(line)
|
|
||||||
if not status:
|
|
||||||
status_info = line.split(" ", 2)
|
|
||||||
status = int(status_info[1])
|
|
||||||
if len(status_info) > 2:
|
|
||||||
status_message = status_info[2]
|
|
||||||
else:
|
|
||||||
kv = line.split(":", 1)
|
|
||||||
if len(kv) != 2:
|
|
||||||
raise WebSocketException("Invalid header")
|
|
||||||
key, value = kv
|
|
||||||
if key.lower() == "set-cookie" and headers.get("set-cookie"):
|
|
||||||
headers["set-cookie"] = headers.get("set-cookie") + "; " + value.strip()
|
|
||||||
else:
|
|
||||||
headers[key.lower()] = value.strip()
|
|
||||||
|
|
||||||
trace("-----------------------")
|
|
||||||
|
|
||||||
return status, headers, status_message
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
"""
|
|
||||||
_logging.py
|
|
||||||
websocket - WebSocket client library for Python
|
|
||||||
|
|
||||||
Copyright 2024 engn33r
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_logger = logging.getLogger("websocket")
|
|
||||||
try:
|
|
||||||
from logging import NullHandler
|
|
||||||
except ImportError:
|
|
||||||
|
|
||||||
class NullHandler(logging.Handler):
|
|
||||||
def emit(self, record) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
_logger.addHandler(NullHandler())
|
|
||||||
|
|
||||||
_traceEnabled = False
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"enableTrace",
|
|
||||||
"dump",
|
|
||||||
"error",
|
|
||||||
"warning",
|
|
||||||
"debug",
|
|
||||||
"trace",
|
|
||||||
"isEnabledForError",
|
|
||||||
"isEnabledForDebug",
|
|
||||||
"isEnabledForTrace",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def enableTrace(
|
|
||||||
traceable: bool,
|
|
||||||
handler: logging.StreamHandler = logging.StreamHandler(),
|
|
||||||
level: str = "DEBUG",
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Turn on/off the traceability.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
traceable: bool
|
|
||||||
If set to True, traceability is enabled.
|
|
||||||
"""
|
|
||||||
global _traceEnabled
|
|
||||||
_traceEnabled = traceable
|
|
||||||
if traceable:
|
|
||||||
_logger.addHandler(handler)
|
|
||||||
_logger.setLevel(getattr(logging, level))
|
|
||||||
|
|
||||||
|
|
||||||
def dump(title: str, message: str) -> None:
|
|
||||||
if _traceEnabled:
|
|
||||||
_logger.debug(f"--- {title} ---")
|
|
||||||
_logger.debug(message)
|
|
||||||
_logger.debug("-----------------------")
|
|
||||||
|
|
||||||
|
|
||||||
def error(msg: str) -> None:
|
|
||||||
_logger.error(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def warning(msg: str) -> None:
|
|
||||||
_logger.warning(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def debug(msg: str) -> None:
|
|
||||||
_logger.debug(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def info(msg: str) -> None:
|
|
||||||
_logger.info(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def trace(msg: str) -> None:
|
|
||||||
if _traceEnabled:
|
|
||||||
_logger.debug(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def isEnabledForError() -> bool:
|
|
||||||
return _logger.isEnabledFor(logging.ERROR)
|
|
||||||
|
|
||||||
|
|
||||||
def isEnabledForDebug() -> bool:
|
|
||||||
return _logger.isEnabledFor(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
def isEnabledForTrace() -> bool:
|
|
||||||
return _traceEnabled
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
import errno
|
|
||||||
import selectors
|
|
||||||
import socket
|
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
from ._exceptions import (
|
|
||||||
WebSocketConnectionClosedException,
|
|
||||||
WebSocketTimeoutException,
|
|
||||||
)
|
|
||||||
from ._ssl_compat import SSLError, SSLWantReadError, SSLWantWriteError
|
|
||||||
from ._utils import extract_error_code, extract_err_message
|
|
||||||
|
|
||||||
"""
|
|
||||||
_socket.py
|
|
||||||
websocket - WebSocket client library for Python
|
|
||||||
|
|
||||||
Copyright 2024 engn33r
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
"""
|
|
||||||
|
|
||||||
DEFAULT_SOCKET_OPTION = [(socket.SOL_TCP, socket.TCP_NODELAY, 1)]
|
|
||||||
if hasattr(socket, "SO_KEEPALIVE"):
|
|
||||||
DEFAULT_SOCKET_OPTION.append((socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1))
|
|
||||||
if hasattr(socket, "TCP_KEEPIDLE"):
|
|
||||||
DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPIDLE, 30))
|
|
||||||
if hasattr(socket, "TCP_KEEPINTVL"):
|
|
||||||
DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPINTVL, 10))
|
|
||||||
if hasattr(socket, "TCP_KEEPCNT"):
|
|
||||||
DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPCNT, 3))
|
|
||||||
|
|
||||||
_default_timeout = None
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"DEFAULT_SOCKET_OPTION",
|
|
||||||
"sock_opt",
|
|
||||||
"setdefaulttimeout",
|
|
||||||
"getdefaulttimeout",
|
|
||||||
"recv",
|
|
||||||
"recv_line",
|
|
||||||
"send",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class sock_opt:
|
|
||||||
def __init__(self, sockopt: list, sslopt: dict) -> None:
|
|
||||||
if sockopt is None:
|
|
||||||
sockopt = []
|
|
||||||
if sslopt is None:
|
|
||||||
sslopt = {}
|
|
||||||
self.sockopt = sockopt
|
|
||||||
self.sslopt = sslopt
|
|
||||||
self.timeout = None
|
|
||||||
|
|
||||||
|
|
||||||
def setdefaulttimeout(timeout: Union[int, float, None]) -> None:
|
|
||||||
"""
|
|
||||||
Set the global timeout setting to connect.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
timeout: int or float
|
|
||||||
default socket timeout time (in seconds)
|
|
||||||
"""
|
|
||||||
global _default_timeout
|
|
||||||
_default_timeout = timeout
|
|
||||||
|
|
||||||
|
|
||||||
def getdefaulttimeout() -> Union[int, float, None]:
|
|
||||||
"""
|
|
||||||
Get default timeout
|
|
||||||
|
|
||||||
Returns
|
|
||||||
----------
|
|
||||||
_default_timeout: int or float
|
|
||||||
Return the global timeout setting (in seconds) to connect.
|
|
||||||
"""
|
|
||||||
return _default_timeout
|
|
||||||
|
|
||||||
|
|
||||||
def recv(sock: socket.socket, bufsize: int) -> bytes:
|
|
||||||
if not sock:
|
|
||||||
raise WebSocketConnectionClosedException("socket is already closed.")
|
|
||||||
|
|
||||||
def _recv():
|
|
||||||
try:
|
|
||||||
return sock.recv(bufsize)
|
|
||||||
except SSLWantReadError:
|
|
||||||
pass
|
|
||||||
except socket.error as exc:
|
|
||||||
error_code = extract_error_code(exc)
|
|
||||||
if error_code not in [errno.EAGAIN, errno.EWOULDBLOCK]:
|
|
||||||
raise
|
|
||||||
|
|
||||||
sel = selectors.DefaultSelector()
|
|
||||||
sel.register(sock, selectors.EVENT_READ)
|
|
||||||
|
|
||||||
r = sel.select(sock.gettimeout())
|
|
||||||
sel.close()
|
|
||||||
|
|
||||||
if r:
|
|
||||||
return sock.recv(bufsize)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if sock.gettimeout() == 0:
|
|
||||||
bytes_ = sock.recv(bufsize)
|
|
||||||
else:
|
|
||||||
bytes_ = _recv()
|
|
||||||
except TimeoutError:
|
|
||||||
raise WebSocketTimeoutException("Connection timed out")
|
|
||||||
except socket.timeout as e:
|
|
||||||
message = extract_err_message(e)
|
|
||||||
raise WebSocketTimeoutException(message)
|
|
||||||
except SSLError as e:
|
|
||||||
message = extract_err_message(e)
|
|
||||||
if isinstance(message, str) and "timed out" in message:
|
|
||||||
raise WebSocketTimeoutException(message)
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
if not bytes_:
|
|
||||||
raise WebSocketConnectionClosedException("Connection to remote host was lost.")
|
|
||||||
|
|
||||||
return bytes_
|
|
||||||
|
|
||||||
|
|
||||||
def recv_line(sock: socket.socket) -> bytes:
|
|
||||||
line = []
|
|
||||||
while True:
|
|
||||||
c = recv(sock, 1)
|
|
||||||
line.append(c)
|
|
||||||
if c == b"\n":
|
|
||||||
break
|
|
||||||
return b"".join(line)
|
|
||||||
|
|
||||||
|
|
||||||
def send(sock: socket.socket, data: Union[bytes, str]) -> int:
|
|
||||||
if isinstance(data, str):
|
|
||||||
data = data.encode("utf-8")
|
|
||||||
|
|
||||||
if not sock:
|
|
||||||
raise WebSocketConnectionClosedException("socket is already closed.")
|
|
||||||
|
|
||||||
def _send():
|
|
||||||
try:
|
|
||||||
return sock.send(data)
|
|
||||||
except SSLWantWriteError:
|
|
||||||
pass
|
|
||||||
except socket.error as exc:
|
|
||||||
error_code = extract_error_code(exc)
|
|
||||||
if error_code is None:
|
|
||||||
raise
|
|
||||||
if error_code not in [errno.EAGAIN, errno.EWOULDBLOCK]:
|
|
||||||
raise
|
|
||||||
|
|
||||||
sel = selectors.DefaultSelector()
|
|
||||||
sel.register(sock, selectors.EVENT_WRITE)
|
|
||||||
|
|
||||||
w = sel.select(sock.gettimeout())
|
|
||||||
sel.close()
|
|
||||||
|
|
||||||
if w:
|
|
||||||
return sock.send(data)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if sock.gettimeout() == 0:
|
|
||||||
return sock.send(data)
|
|
||||||
else:
|
|
||||||
return _send()
|
|
||||||
except socket.timeout as e:
|
|
||||||
message = extract_err_message(e)
|
|
||||||
raise WebSocketTimeoutException(message)
|
|
||||||
except Exception as e:
|
|
||||||
message = extract_err_message(e)
|
|
||||||
if isinstance(message, str) and "timed out" in message:
|
|
||||||
raise WebSocketTimeoutException(message)
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
"""
|
|
||||||
_ssl_compat.py
|
|
||||||
websocket - WebSocket client library for Python
|
|
||||||
|
|
||||||
Copyright 2024 engn33r
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"HAVE_SSL",
|
|
||||||
"ssl",
|
|
||||||
"SSLError",
|
|
||||||
"SSLEOFError",
|
|
||||||
"SSLWantReadError",
|
|
||||||
"SSLWantWriteError",
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
import ssl
|
|
||||||
from ssl import SSLError, SSLEOFError, SSLWantReadError, SSLWantWriteError
|
|
||||||
|
|
||||||
HAVE_SSL = True
|
|
||||||
except ImportError:
|
|
||||||
# dummy class of SSLError for environment without ssl support
|
|
||||||
class SSLError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class SSLEOFError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class SSLWantReadError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class SSLWantWriteError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
ssl = None
|
|
||||||
HAVE_SSL = False
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
import os
|
|
||||||
import socket
|
|
||||||
import struct
|
|
||||||
from typing import Optional
|
|
||||||
from urllib.parse import unquote, urlparse
|
|
||||||
from ._exceptions import WebSocketProxyException
|
|
||||||
|
|
||||||
"""
|
|
||||||
_url.py
|
|
||||||
websocket - WebSocket client library for Python
|
|
||||||
|
|
||||||
Copyright 2024 engn33r
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__all__ = ["parse_url", "get_proxy_info"]
|
|
||||||
|
|
||||||
|
|
||||||
def parse_url(url: str) -> tuple:
|
|
||||||
"""
|
|
||||||
parse url and the result is tuple of
|
|
||||||
(hostname, port, resource path and the flag of secure mode)
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
url: str
|
|
||||||
url string.
|
|
||||||
"""
|
|
||||||
if ":" not in url:
|
|
||||||
raise ValueError("url is invalid")
|
|
||||||
|
|
||||||
scheme, url = url.split(":", 1)
|
|
||||||
|
|
||||||
parsed = urlparse(url, scheme="http")
|
|
||||||
if parsed.hostname:
|
|
||||||
hostname = parsed.hostname
|
|
||||||
else:
|
|
||||||
raise ValueError("hostname is invalid")
|
|
||||||
port = 0
|
|
||||||
if parsed.port:
|
|
||||||
port = parsed.port
|
|
||||||
|
|
||||||
is_secure = False
|
|
||||||
if scheme == "ws":
|
|
||||||
if not port:
|
|
||||||
port = 80
|
|
||||||
elif scheme == "wss":
|
|
||||||
is_secure = True
|
|
||||||
if not port:
|
|
||||||
port = 443
|
|
||||||
else:
|
|
||||||
raise ValueError("scheme %s is invalid" % scheme)
|
|
||||||
|
|
||||||
if parsed.path:
|
|
||||||
resource = parsed.path
|
|
||||||
else:
|
|
||||||
resource = "/"
|
|
||||||
|
|
||||||
if parsed.query:
|
|
||||||
resource += f"?{parsed.query}"
|
|
||||||
|
|
||||||
return hostname, port, resource, is_secure
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_NO_PROXY_HOST = ["localhost", "127.0.0.1"]
|
|
||||||
|
|
||||||
|
|
||||||
def _is_ip_address(addr: str) -> bool:
|
|
||||||
try:
|
|
||||||
socket.inet_aton(addr)
|
|
||||||
except socket.error:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def _is_subnet_address(hostname: str) -> bool:
|
|
||||||
try:
|
|
||||||
addr, netmask = hostname.split("/")
|
|
||||||
return _is_ip_address(addr) and 0 <= int(netmask) < 32
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _is_address_in_network(ip: str, net: str) -> bool:
|
|
||||||
ipaddr: int = struct.unpack("!I", socket.inet_aton(ip))[0]
|
|
||||||
netaddr, netmask = net.split("/")
|
|
||||||
netaddr: int = struct.unpack("!I", socket.inet_aton(netaddr))[0]
|
|
||||||
|
|
||||||
netmask = (0xFFFFFFFF << (32 - int(netmask))) & 0xFFFFFFFF
|
|
||||||
return ipaddr & netmask == netaddr
|
|
||||||
|
|
||||||
|
|
||||||
def _is_no_proxy_host(hostname: str, no_proxy: Optional[list]) -> bool:
|
|
||||||
if not no_proxy:
|
|
||||||
if v := os.environ.get("no_proxy", os.environ.get("NO_PROXY", "")).replace(
|
|
||||||
" ", ""
|
|
||||||
):
|
|
||||||
no_proxy = v.split(",")
|
|
||||||
if not no_proxy:
|
|
||||||
no_proxy = DEFAULT_NO_PROXY_HOST
|
|
||||||
|
|
||||||
if "*" in no_proxy:
|
|
||||||
return True
|
|
||||||
if hostname in no_proxy:
|
|
||||||
return True
|
|
||||||
if _is_ip_address(hostname):
|
|
||||||
return any(
|
|
||||||
[
|
|
||||||
_is_address_in_network(hostname, subnet)
|
|
||||||
for subnet in no_proxy
|
|
||||||
if _is_subnet_address(subnet)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
for domain in [domain for domain in no_proxy if domain.startswith(".")]:
|
|
||||||
if hostname.endswith(domain):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_proxy_info(
|
|
||||||
hostname: str,
|
|
||||||
is_secure: bool,
|
|
||||||
proxy_host: Optional[str] = None,
|
|
||||||
proxy_port: int = 0,
|
|
||||||
proxy_auth: Optional[tuple] = None,
|
|
||||||
no_proxy: Optional[list] = None,
|
|
||||||
proxy_type: str = "http",
|
|
||||||
) -> tuple:
|
|
||||||
"""
|
|
||||||
Try to retrieve proxy host and port from environment
|
|
||||||
if not provided in options.
|
|
||||||
Result is (proxy_host, proxy_port, proxy_auth).
|
|
||||||
proxy_auth is tuple of username and password
|
|
||||||
of proxy authentication information.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
hostname: str
|
|
||||||
Websocket server name.
|
|
||||||
is_secure: bool
|
|
||||||
Is the connection secure? (wss) looks for "https_proxy" in env
|
|
||||||
instead of "http_proxy"
|
|
||||||
proxy_host: str
|
|
||||||
http proxy host name.
|
|
||||||
proxy_port: str or int
|
|
||||||
http proxy port.
|
|
||||||
no_proxy: list
|
|
||||||
Whitelisted host names that don't use the proxy.
|
|
||||||
proxy_auth: tuple
|
|
||||||
HTTP proxy auth information. Tuple of username and password. Default is None.
|
|
||||||
proxy_type: str
|
|
||||||
Specify the proxy protocol (http, socks4, socks4a, socks5, socks5h). Default is "http".
|
|
||||||
Use socks4a or socks5h if you want to send DNS requests through the proxy.
|
|
||||||
"""
|
|
||||||
if _is_no_proxy_host(hostname, no_proxy):
|
|
||||||
return None, 0, None
|
|
||||||
|
|
||||||
if proxy_host:
|
|
||||||
if not proxy_port:
|
|
||||||
raise WebSocketProxyException("Cannot use port 0 when proxy_host specified")
|
|
||||||
port = proxy_port
|
|
||||||
auth = proxy_auth
|
|
||||||
return proxy_host, port, auth
|
|
||||||
|
|
||||||
env_key = "https_proxy" if is_secure else "http_proxy"
|
|
||||||
value = os.environ.get(env_key, os.environ.get(env_key.upper(), "")).replace(
|
|
||||||
" ", ""
|
|
||||||
)
|
|
||||||
if value:
|
|
||||||
proxy = urlparse(value)
|
|
||||||
auth = (
|
|
||||||
(unquote(proxy.username), unquote(proxy.password))
|
|
||||||
if proxy.username
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
return proxy.hostname, proxy.port, auth
|
|
||||||
|
|
||||||
return None, 0, None
|
|
||||||
@@ -1,459 +0,0 @@
|
|||||||
from typing import Union
|
|
||||||
|
|
||||||
"""
|
|
||||||
_url.py
|
|
||||||
websocket - WebSocket client library for Python
|
|
||||||
|
|
||||||
Copyright 2024 engn33r
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
"""
|
|
||||||
__all__ = ["NoLock", "validate_utf8", "extract_err_message", "extract_error_code"]
|
|
||||||
|
|
||||||
|
|
||||||
class NoLock:
|
|
||||||
def __enter__(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
# If wsaccel is available we use compiled routines to validate UTF-8
|
|
||||||
# strings.
|
|
||||||
from wsaccel.utf8validator import Utf8Validator
|
|
||||||
|
|
||||||
def _validate_utf8(utfbytes: Union[str, bytes]) -> bool:
|
|
||||||
result: bool = Utf8Validator().validate(utfbytes)[0]
|
|
||||||
return result
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
# UTF-8 validator
|
|
||||||
# python implementation of http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
|
|
||||||
|
|
||||||
_UTF8_ACCEPT = 0
|
|
||||||
_UTF8_REJECT = 12
|
|
||||||
|
|
||||||
_UTF8D = [
|
|
||||||
# The first part of the table maps bytes to character classes that
|
|
||||||
# to reduce the size of the transition table and create bitmasks.
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
9,
|
|
||||||
9,
|
|
||||||
9,
|
|
||||||
9,
|
|
||||||
9,
|
|
||||||
9,
|
|
||||||
9,
|
|
||||||
9,
|
|
||||||
9,
|
|
||||||
9,
|
|
||||||
9,
|
|
||||||
9,
|
|
||||||
9,
|
|
||||||
9,
|
|
||||||
9,
|
|
||||||
9,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
7,
|
|
||||||
8,
|
|
||||||
8,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
10,
|
|
||||||
3,
|
|
||||||
3,
|
|
||||||
3,
|
|
||||||
3,
|
|
||||||
3,
|
|
||||||
3,
|
|
||||||
3,
|
|
||||||
3,
|
|
||||||
3,
|
|
||||||
3,
|
|
||||||
3,
|
|
||||||
3,
|
|
||||||
4,
|
|
||||||
3,
|
|
||||||
3,
|
|
||||||
11,
|
|
||||||
6,
|
|
||||||
6,
|
|
||||||
6,
|
|
||||||
5,
|
|
||||||
8,
|
|
||||||
8,
|
|
||||||
8,
|
|
||||||
8,
|
|
||||||
8,
|
|
||||||
8,
|
|
||||||
8,
|
|
||||||
8,
|
|
||||||
8,
|
|
||||||
8,
|
|
||||||
8,
|
|
||||||
# The second part is a transition table that maps a combination
|
|
||||||
# of a state of the automaton and a character class to a state.
|
|
||||||
0,
|
|
||||||
12,
|
|
||||||
24,
|
|
||||||
36,
|
|
||||||
60,
|
|
||||||
96,
|
|
||||||
84,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
48,
|
|
||||||
72,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
0,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
0,
|
|
||||||
12,
|
|
||||||
0,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
24,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
24,
|
|
||||||
12,
|
|
||||||
24,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
24,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
24,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
24,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
36,
|
|
||||||
12,
|
|
||||||
36,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
36,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
36,
|
|
||||||
12,
|
|
||||||
36,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
36,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
]
|
|
||||||
|
|
||||||
def _decode(state: int, codep: int, ch: int) -> tuple:
|
|
||||||
tp = _UTF8D[ch]
|
|
||||||
|
|
||||||
codep = (
|
|
||||||
(ch & 0x3F) | (codep << 6) if (state != _UTF8_ACCEPT) else (0xFF >> tp) & ch
|
|
||||||
)
|
|
||||||
state = _UTF8D[256 + state + tp]
|
|
||||||
|
|
||||||
return state, codep
|
|
||||||
|
|
||||||
def _validate_utf8(utfbytes: Union[str, bytes]) -> bool:
|
|
||||||
state = _UTF8_ACCEPT
|
|
||||||
codep = 0
|
|
||||||
for i in utfbytes:
|
|
||||||
state, codep = _decode(state, codep, int(i))
|
|
||||||
if state == _UTF8_REJECT:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def validate_utf8(utfbytes: Union[str, bytes]) -> bool:
|
|
||||||
"""
|
|
||||||
validate utf8 byte string.
|
|
||||||
utfbytes: utf byte string to check.
|
|
||||||
return value: if valid utf8 string, return true. Otherwise, return false.
|
|
||||||
"""
|
|
||||||
return _validate_utf8(utfbytes)
|
|
||||||
|
|
||||||
|
|
||||||
def extract_err_message(exception: Exception) -> Union[str, None]:
|
|
||||||
if exception.args:
|
|
||||||
exception_message: str = exception.args[0]
|
|
||||||
return exception_message
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def extract_error_code(exception: Exception) -> Union[int, None]:
|
|
||||||
if exception.args and len(exception.args) > 1:
|
|
||||||
return exception.args[0] if isinstance(exception.args[0], int) else None
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
"""
|
|
||||||
wsdump.py
|
|
||||||
websocket - WebSocket client library for Python
|
|
||||||
|
|
||||||
Copyright 2024 engn33r
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import code
|
|
||||||
import gzip
|
|
||||||
import ssl
|
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import zlib
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
import websocket
|
|
||||||
|
|
||||||
try:
|
|
||||||
import readline
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def get_encoding() -> str:
|
|
||||||
encoding = getattr(sys.stdin, "encoding", "")
|
|
||||||
if not encoding:
|
|
||||||
return "utf-8"
|
|
||||||
else:
|
|
||||||
return encoding.lower()
|
|
||||||
|
|
||||||
|
|
||||||
OPCODE_DATA = (websocket.ABNF.OPCODE_TEXT, websocket.ABNF.OPCODE_BINARY)
|
|
||||||
ENCODING = get_encoding()
|
|
||||||
|
|
||||||
|
|
||||||
class VAction(argparse.Action):
|
|
||||||
def __call__(
|
|
||||||
self,
|
|
||||||
parser: argparse.Namespace,
|
|
||||||
args: tuple,
|
|
||||||
values: str,
|
|
||||||
option_string: str = None,
|
|
||||||
) -> None:
|
|
||||||
if values is None:
|
|
||||||
values = "1"
|
|
||||||
try:
|
|
||||||
values = int(values)
|
|
||||||
except ValueError:
|
|
||||||
values = values.count("v") + 1
|
|
||||||
setattr(args, self.dest, values)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser(description="WebSocket Simple Dump Tool")
|
|
||||||
parser.add_argument(
|
|
||||||
"url", metavar="ws_url", help="websocket url. ex. ws://echo.websocket.events/"
|
|
||||||
)
|
|
||||||
parser.add_argument("-p", "--proxy", help="proxy url. ex. http://127.0.0.1:8080")
|
|
||||||
parser.add_argument(
|
|
||||||
"-v",
|
|
||||||
"--verbose",
|
|
||||||
default=0,
|
|
||||||
nargs="?",
|
|
||||||
action=VAction,
|
|
||||||
dest="verbose",
|
|
||||||
help="set verbose mode. If set to 1, show opcode. "
|
|
||||||
"If set to 2, enable to trace websocket module",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"-n", "--nocert", action="store_true", help="Ignore invalid SSL cert"
|
|
||||||
)
|
|
||||||
parser.add_argument("-r", "--raw", action="store_true", help="raw output")
|
|
||||||
parser.add_argument("-s", "--subprotocols", nargs="*", help="Set subprotocols")
|
|
||||||
parser.add_argument("-o", "--origin", help="Set origin")
|
|
||||||
parser.add_argument(
|
|
||||||
"--eof-wait",
|
|
||||||
default=0,
|
|
||||||
type=int,
|
|
||||||
help="wait time(second) after 'EOF' received.",
|
|
||||||
)
|
|
||||||
parser.add_argument("-t", "--text", help="Send initial text")
|
|
||||||
parser.add_argument(
|
|
||||||
"--timings", action="store_true", help="Print timings in seconds"
|
|
||||||
)
|
|
||||||
parser.add_argument("--headers", help="Set custom headers. Use ',' as separator")
|
|
||||||
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
class RawInput:
|
|
||||||
def raw_input(self, prompt: str = "") -> str:
|
|
||||||
line = input(prompt)
|
|
||||||
|
|
||||||
if ENCODING and ENCODING != "utf-8" and not isinstance(line, str):
|
|
||||||
line = line.decode(ENCODING).encode("utf-8")
|
|
||||||
elif isinstance(line, str):
|
|
||||||
line = line.encode("utf-8")
|
|
||||||
|
|
||||||
return line
|
|
||||||
|
|
||||||
|
|
||||||
class InteractiveConsole(RawInput, code.InteractiveConsole):
|
|
||||||
def write(self, data: str) -> None:
|
|
||||||
sys.stdout.write("\033[2K\033[E")
|
|
||||||
# sys.stdout.write("\n")
|
|
||||||
sys.stdout.write("\033[34m< " + data + "\033[39m")
|
|
||||||
sys.stdout.write("\n> ")
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
def read(self) -> str:
|
|
||||||
return self.raw_input("> ")
|
|
||||||
|
|
||||||
|
|
||||||
class NonInteractive(RawInput):
|
|
||||||
def write(self, data: str) -> None:
|
|
||||||
sys.stdout.write(data)
|
|
||||||
sys.stdout.write("\n")
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
def read(self) -> str:
|
|
||||||
return self.raw_input("")
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
start_time = time.time()
|
|
||||||
args = parse_args()
|
|
||||||
if args.verbose > 1:
|
|
||||||
websocket.enableTrace(True)
|
|
||||||
options = {}
|
|
||||||
if args.proxy:
|
|
||||||
p = urlparse(args.proxy)
|
|
||||||
options["http_proxy_host"] = p.hostname
|
|
||||||
options["http_proxy_port"] = p.port
|
|
||||||
if args.origin:
|
|
||||||
options["origin"] = args.origin
|
|
||||||
if args.subprotocols:
|
|
||||||
options["subprotocols"] = args.subprotocols
|
|
||||||
opts = {}
|
|
||||||
if args.nocert:
|
|
||||||
opts = {"cert_reqs": ssl.CERT_NONE, "check_hostname": False}
|
|
||||||
if args.headers:
|
|
||||||
options["header"] = list(map(str.strip, args.headers.split(",")))
|
|
||||||
ws = websocket.create_connection(args.url, sslopt=opts, **options)
|
|
||||||
if args.raw:
|
|
||||||
console = NonInteractive()
|
|
||||||
else:
|
|
||||||
console = InteractiveConsole()
|
|
||||||
print("Press Ctrl+C to quit")
|
|
||||||
|
|
||||||
def recv() -> tuple:
|
|
||||||
try:
|
|
||||||
frame = ws.recv_frame()
|
|
||||||
except websocket.WebSocketException:
|
|
||||||
return websocket.ABNF.OPCODE_CLOSE, ""
|
|
||||||
if not frame:
|
|
||||||
raise websocket.WebSocketException(f"Not a valid frame {frame}")
|
|
||||||
elif frame.opcode in OPCODE_DATA:
|
|
||||||
return frame.opcode, frame.data
|
|
||||||
elif frame.opcode == websocket.ABNF.OPCODE_CLOSE:
|
|
||||||
ws.send_close()
|
|
||||||
return frame.opcode, ""
|
|
||||||
elif frame.opcode == websocket.ABNF.OPCODE_PING:
|
|
||||||
ws.pong(frame.data)
|
|
||||||
return frame.opcode, frame.data
|
|
||||||
|
|
||||||
return frame.opcode, frame.data
|
|
||||||
|
|
||||||
def recv_ws() -> None:
|
|
||||||
while True:
|
|
||||||
opcode, data = recv()
|
|
||||||
msg = None
|
|
||||||
if opcode == websocket.ABNF.OPCODE_TEXT and isinstance(data, bytes):
|
|
||||||
data = str(data, "utf-8")
|
|
||||||
if (
|
|
||||||
isinstance(data, bytes) and len(data) > 2 and data[:2] == b"\037\213"
|
|
||||||
): # gzip magick
|
|
||||||
try:
|
|
||||||
data = "[gzip] " + str(gzip.decompress(data), "utf-8")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
elif isinstance(data, bytes):
|
|
||||||
try:
|
|
||||||
data = "[zlib] " + str(
|
|
||||||
zlib.decompress(data, -zlib.MAX_WBITS), "utf-8"
|
|
||||||
)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if isinstance(data, bytes):
|
|
||||||
data = repr(data)
|
|
||||||
|
|
||||||
if args.verbose:
|
|
||||||
msg = f"{websocket.ABNF.OPCODE_MAP.get(opcode)}: {data}"
|
|
||||||
else:
|
|
||||||
msg = data
|
|
||||||
|
|
||||||
if msg is not None:
|
|
||||||
if args.timings:
|
|
||||||
console.write(f"{time.time() - start_time}: {msg}")
|
|
||||||
else:
|
|
||||||
console.write(msg)
|
|
||||||
|
|
||||||
if opcode == websocket.ABNF.OPCODE_CLOSE:
|
|
||||||
break
|
|
||||||
|
|
||||||
thread = threading.Thread(target=recv_ws)
|
|
||||||
thread.daemon = True
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
if args.text:
|
|
||||||
ws.send(args.text)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
message = console.read()
|
|
||||||
ws.send(message)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
return
|
|
||||||
except EOFError:
|
|
||||||
time.sleep(args.eof_wait)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
try:
|
|
||||||
main()
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user