Compare commits

...

4 commits

4 changed files with 68 additions and 7 deletions

45
README.md Normal file
View file

@ -0,0 +1,45 @@
Bebop
=====
Bebop is a [Gemini][gemini] browser for the terminal, focusing on practicality
and speed. It is a personal project to learn how to use ncurses and try new
ways to explore the Geminispace. It borrows some ideas from [Amfora][amfora],
another great terminal browser, Vim for interactivity and tries to support mouse
usage decently.
[gemini]: https://gemini.circumlunar.space/
[amfora]: https://github.com/makeworld-the-better-one/amfora
If you are interested in Gemini and looking for a client, I recommend trying a
graphical one like the excellent [Lagrange][lagrange] or [Kristall][kristall],
or Amfora if you're feeling more at home in the terminal. Bebop won't attempt
to support every feature other clients might have.
[lagrange]: https://git.skyjake.fi/skyjake/lagrange
[kristall]: https://kristall.random-projects.net/
It passes the Conman's client test but not Egsam's for now.
Features
--------
### What works
- Basic browsing: scrolling, follow links, redirections, Web links.
### What is planned
- Handle more content types.
- Great config options.
- Identity management with temporary and managed certificates.
- Buffers (or tabs if you prefer).
- Home page.
- Bookmarks.
### What is not planned for now
- 256-colors mode and themes.
- Subscriptions. I have no need for them as I prefer to use a browser-agnostic
aggregator at the moment.

View file

@ -222,7 +222,11 @@ class Browser:
else:
pass # TODO
response = Response.parse(req.proceed())
data = req.proceed()
if not data:
self.set_status_error(f"Server did not respond in time ({url}).")
return
response = Response.parse(data)
if not response:
self.set_status_error(f"Server response parsing failed ({url}).")
return

View file

@ -81,7 +81,7 @@ class Request:
self.payload += LINE_TERM
try:
sock = socket.create_connection((hostname, port))
sock = socket.create_connection((hostname, port), timeout=10)
except OSError as exc:
self.state = Request.STATE_CONNECTION_FAILED
self.error = exc.strerror
@ -124,7 +124,10 @@ class Request:
self.ssock.sendall(self.payload)
response = b""
while True:
buf = self.ssock.recv(4096)
try:
buf = self.ssock.recv(4096)
except socket.timeout:
buf = None
if not buf:
return response
response += buf
@ -172,6 +175,7 @@ class Response:
content: bytes = b""
HEADER_RE = re.compile(r"(\d{2}) (.*)")
MAX_META_LEN = 1024
@property
def generic_code(self):
@ -189,6 +193,8 @@ class Response:
if not match:
return None
code, meta = match.groups()
if len(meta) > Response.MAX_META_LEN:
return None
response = Response(StatusCode(int(code)), meta=meta)
if response.generic_code == StatusCode.SUCCESS:
content_offset = response_header_len + len(LINE_TERM)

View file

@ -215,10 +215,13 @@ def render_lines(metalines, window, max_width):
- max_width: line length limit for the pad.
Return:
The tuple (height, width) of the resized window.
The tuple of integers (error, height, width), error being a non-zero value
if an error occured during rendering, and height and width being the new
dimensions of the resized window.
"""
num_lines = len(metalines)
window.resize(num_lines, max_width)
new_dimensions = num_lines, max_width
window.resize(*new_dimensions)
for line_index, metaline in enumerate(metalines):
meta, line = metaline
line = line[:max_width - 1]
@ -237,7 +240,10 @@ def render_lines(metalines, window, max_width):
attr = curses.color_pair(ColorPair.BLOCKQUOTE) | curses.A_ITALIC
else: # includes LineType.PARAGRAPH
attr = curses.color_pair(ColorPair.NORMAL)
window.addstr(line, attr)
try:
window.addstr(line, attr)
except ValueError:
return new_dimensions
if line_index < num_lines - 1:
window.addstr("\n")
return num_lines, max_width
return new_dimensions