Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f0d3a48
Implement Named Pipe and Websocket support
Oekn5w Jun 28, 2026
1910a4e
Preseve position on restart
Oekn5w Jun 28, 2026
f6e7b9a
Move status updates to single function
Oekn5w Jun 28, 2026
3c71b2e
Catch any exception that occurs in connections
Oekn5w Jun 28, 2026
2a445c4
Add reload option for updating notesfile in-between
Oekn5w Jun 28, 2026
2a4de33
Update Readme
Oekn5w Jun 28, 2026
7d39419
Move to individual connection classes; Platform dependent Pipes
Oekn5w Jun 30, 2026
41735fb
Revert settings UI changes
Oekn5w Jun 30, 2026
cff98fb
Make livesplit_client into a package.
DavidCEllis Jul 1, 2026
dedd5c7
Split the connection out into separate files
DavidCEllis Jul 1, 2026
0dbfa97
Remove unnecessary platform checks
DavidCEllis Jul 1, 2026
8376f28
Update uv.lock
DavidCEllis Jul 1, 2026
bf8c604
Make ConnectionPipe a prefab
DavidCEllis Jul 1, 2026
9eba25e
Convert slotted prefabs to decorator form to allow for a new ABC meta…
DavidCEllis Jul 1, 2026
b17e1ae
Make `ConnectionTypeBase` an ABC and define the methods as abstract.
DavidCEllis Jul 1, 2026
9e6837d
de-duplication of errors
DavidCEllis Jul 1, 2026
54dd168
Start putting logic in place to simplify `LivesplitConnection`
DavidCEllis Jul 1, 2026
bc3c68b
Create a CONNECTION_TYPES list of appropriate connection types.
DavidCEllis Jul 1, 2026
c526a04
Make connection_obj visible and replaceable on `__init__`
DavidCEllis Jul 1, 2026
4b59aaf
Only test TCP in the networking tests - for some reason the websocket…
DavidCEllis Jul 1, 2026
93bd5cd
Major simplification of connect logic for LivesplitConnection.connect
DavidCEllis Jul 1, 2026
ea552c5
The attribute is called 'server' not 'hostname' *headdesk*
DavidCEllis Jul 1, 2026
7024708
type checking leading me astray?
DavidCEllis Jul 1, 2026
53d4c85
Remove unnecessary parentheses around bool
DavidCEllis Jul 1, 2026
471a8f6
Make it possible to narrow the connection types on the connection.
DavidCEllis Jul 1, 2026
f567706
Use a cast instead of an assert.
DavidCEllis Jul 1, 2026
6867740
Merge branch 'servertypes-edit' into dev_servertypes
Oekn5w Jul 8, 2026
c23519e
Adapt tests
Oekn5w Jul 8, 2026
c1cca15
Adapt exceptions
Oekn5w Jul 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ dependencies = [
"ducktools-classbuilder>=0.7.4",
"waitress~=3.0",
"platformdirs~=4.3",
"websocket-client>=1.9.0",
"pywin32>=311; sys_platform == 'win32'",
]
classifiers = [
"Development Status :: 4 - Beta",
Expand Down
16 changes: 13 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ platforms.
containing the notes you wish to use.
3. Some configuration is available from the settings dialog.

If you are already using the Livesplit Server for something else,
this appliction shouldn't interfere with that, but only a TCP or a Websocket server can be running at a time.
You can change the connection to Livesplit that will be used in the settings.
There are 3 options:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not give the user any options and instead just try/fail the different options internally.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've now added a connection logic cycling through the 2 (or 3 if applicable) methods, although I haven't updated the readme yet

* Named Pipe
* TCP Server (default)
* Websocket Server
The Named Pipe does not use the hostname and port, it should "just work", as long as livesplit is running on the same PC.

Plain text formatting works the same way as SplitNotes.
Notes made for that should function fine in SplitGuides.

Expand All @@ -50,10 +59,11 @@ inserted in between lines.
* If a split separator is given, newlines are left as in the input to the
markdown/html processors.

If you edited your notes while the program was running, you can reload the notes in the context menu "Reload Notes File".

### If the notes are not advancing ###

If the text at the bottom says "Trying to connect to Livesplit." make sure that the TCP server
is running.
If the text at the bottom says "Trying to connect to Livesplit." make sure that the selected server in the settings is running.

If that doesn't work, check in the Splitguides settings (from the right click menu)
* "Livesplit Server Hostname" should be `localhost`
Expand Down Expand Up @@ -81,7 +91,7 @@ and port given in a web browser.

Configuration Options:

* Livesplit server hostname and port
* Livesplit server hostname, port and wanted connection type
* Display previous/next splits
* Split separator (leave blank for empty line separator)
* Font Size
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import re
import socket
import sys

from datetime import timedelta
import typing

from ducktools.classbuilder.prefab import Prefab, attribute

BUFFER_SIZE = 4096
from .connection_shared import (
ConnectionTypeBase,
ConnectionTCP,
ConnectionWS
)

CONNECTION_TYPES: tuple[type[ConnectionTypeBase], ...]

if sys.platform == "win32":
from .connection_windows import ConnectionPipe
CONNECTION_TYPES = (ConnectionPipe, ConnectionTCP, ConnectionWS)
else:
CONNECTION_TYPES = (ConnectionTCP, ConnectionWS)

pattern = re.compile(
r"^(?:(?P<hours>\d*):)?(?P<minutes>\d{1,2}):(?P<seconds>\d{2}).(?P<centiseconds>\d*)"
Expand Down Expand Up @@ -38,90 +50,72 @@ def parse_time(time_str: str) -> timedelta:

class LivesplitConnection(Prefab):
"""
Socket based livesplit connection model
Livesplit connection model supporting Named Pipe (Windows-only), TCP and Websocket connections
"""
server: str = "localhost"
port: int = 16834
timeout: int = 1
sock: socket.socket | None = attribute(default=None, init=False, repr=False)

# Make it possible to replace the possible connection types for testing, but see the actual type for debugging
connection_obj: ConnectionTypeBase | None = attribute(default=None, init=False)
connection_types: tuple[type[ConnectionTypeBase], ...] = attribute(default=CONNECTION_TYPES, repr=False)

def is_connected(self) -> bool:
return bool(self.connection_obj)

def get_connection_friendly_name(self) -> str:
if self.connection_obj:
status = self.connection_obj.NAME
else:
status = ""
return status

def connect(self) -> bool:
"""
Attempt to connect to the livesplit server
:return: True if connected, otherwise False
"""
self.sock = socket.socket()
try:
self.sock.connect((self.server, self.port))
except ConnectionRefusedError:
self.sock.close()
self.sock = None
return False
except socket.gaierror:
# Could not resolve hostname
self.sock.close()
self.sock = None
return False
self.close()

for connection_type in self.connection_types:
# Try each connection type in succession, accept the first successful connection type
connection = connection_type(self.server, self.port)
connection_success = connection.connect()
if connection_success:
self.connection_obj = connection
break
else:
self.sock.settimeout(self.timeout)
return True
connection_success = False

return connection_success

def close(self) -> None:
if self.sock:
self.sock.close()
self.sock = None
if self.connection_obj:
self.connection_obj.close()
self.connection_obj = None

def send(self, msg: bytes) -> None:
"""
Send a message to the livesplit server - connect if not already connected.
If the connection is aborted (ie: if livesplit server has been closed)
raise a ConnectionAbortedError

:param msg: bytes message to send (should end with "\r\n")
:param msg: bytes message to send
:return:
"""
if not self.sock:
self.connect()

if self.sock: # Check again in case connection failed
try:
self.sock.send(msg)
except ConnectionAbortedError:
self.sock.close()
self.sock = None
raise ConnectionAbortedError("The connection has been closed by the host")
if self.connection_obj:
self.connection_obj.send(msg)

def receive(self) -> bytes:
"""
Attempt to receive a message from the livesplit server
raise ConnectionError if the connection has been terminated.

:return: bytes received from the server
"""
if not self.sock:
self.connect()

if self.sock:
try:
data_received = self.sock.recv(BUFFER_SIZE)
except socket.timeout:
raise TimeoutError(
"No response received from the server within "
f"the timeout period ({self.timeout}s)"
)
except OSError:
self.sock.close()
self.sock = None
raise ConnectionError("The connection has been closed by the host")

if data_received == b"":
self.sock.close()
self.sock = None
raise ConnectionError("The connection has been closed by the host")

return data_received

return b""
:return: bytes or string received from the server
"""
if self.connection_obj:
return self.connection_obj.receive()
else:
return b""


class LivesplitMessaging(Prefab):
Expand All @@ -133,9 +127,9 @@ def connect(self) -> bool:
def close(self) -> None:
self.connection.close()

def send(self, message) -> None:
def send(self, message: str) -> None:
m = message.encode("UTF8")
self.connection.send(m + b"\r\n")
self.connection.send(m)

@typing.overload
def receive(self, datatype: typing.Literal["time"]) -> timedelta: ...
Expand All @@ -146,7 +140,8 @@ def receive(self, datatype: typing.Literal["text"] = "text") -> str: ...

def receive(self, datatype="text"):
result = self.connection.receive()
result = result.strip().decode("UTF8")
result = result.decode("UTF8").strip()

if datatype == "time":
result = parse_time(result)
elif datatype == "int":
Expand Down Expand Up @@ -247,7 +242,7 @@ def get_delta(self, comparison=None) -> str:
if comparison:
self.send(f"getdelta {comparison}")
else:
self.send(f"getdelta")
self.send("getdelta")

return self.receive()

Expand Down Expand Up @@ -297,7 +292,6 @@ def get_current_timer_phase(self) -> str:

def get_client(
server: str = "localhost",
port: int = 16834,
timeout: int = 1
port: int = 16834
) -> LivesplitMessaging:
return LivesplitMessaging(connection=LivesplitConnection(server, port, timeout))
return LivesplitMessaging(connection=LivesplitConnection(server, port))
Loading