Compare commits

...

4 commits

7 changed files with 86 additions and 22 deletions

View file

@ -4,7 +4,6 @@ import curses
import curses.ascii import curses.ascii
import curses.textpad import curses.textpad
import os import os
import subprocess
import tempfile import tempfile
from math import inf from math import inf
@ -13,6 +12,7 @@ from bebop.bookmarks import (
) )
from bebop.colors import ColorPair, init_colors from bebop.colors import ColorPair, init_colors
from bebop.command_line import CommandLine from bebop.command_line import CommandLine
from bebop.external import open_external_program
from bebop.history import History from bebop.history import History
from bebop.links import Links from bebop.links import Links
from bebop.mouse import ButtonState from bebop.mouse import ButtonState
@ -102,7 +102,7 @@ class Browser:
elif char == ord("h"): elif char == ord("h"):
self.scroll_page_horizontally(-3) self.scroll_page_horizontally(-3)
elif char == ord("H"): elif char == ord("H"):
pass # TODO h-scroll whole page left self.scroll_whole_page_left()
elif char == ord("j"): elif char == ord("j"):
self.scroll_page_vertically(3) self.scroll_page_vertically(3)
elif char == ord("J"): elif char == ord("J"):
@ -114,9 +114,9 @@ class Browser:
elif char == ord("l"): elif char == ord("l"):
self.scroll_page_horizontally(3) self.scroll_page_horizontally(3)
elif char == ord("L"): elif char == ord("L"):
pass # TODO h-scroll whole page right self.scroll_whole_page_right()
elif char == ord("^"): elif char == ord("^"):
pass # TODO reset horizontal scrolling self.scroll_page_horizontally(-inf)
elif char == ord("g"): elif char == ord("g"):
char = self.screen.getch() char = self.screen.getch()
if char == ord("g"): if char == ord("g"):
@ -382,10 +382,27 @@ class Browser:
self.scroll_page_vertically(-self.page_pad_size[0]) self.scroll_page_vertically(-self.page_pad_size[0])
def scroll_page_horizontally(self, by_columns): def scroll_page_horizontally(self, by_columns):
"""Scroll page horizontally.""" """Scroll page horizontally.
if self.page_pad.scroll_h(by_columns, self.w):
If `by_lines` is an integer (positive or negative), scroll the page by
this amount of columns. If `by_lines` is -inf, scroll back to the first
column. Scrolling to the right-most column is not supported.
"""
if by_columns == -inf:
require_refresh = self.page_pad.go_to_first_column()
else:
require_refresh = self.page_pad.scroll_h(by_columns, self.w)
if require_refresh:
self.refresh_page() self.refresh_page()
def scroll_whole_page_left(self):
"""Scroll left by a whole page."""
self.scroll_page_horizontally(-self.page_pad_size[1])
def scroll_whole_page_right(self):
"""Scroll right by a whole page."""
self.scroll_page_horizontally(self.page_pad_size[1])
def reload_page(self): def reload_page(self):
"""Reload the page, if one has been previously loaded.""" """Reload the page, if one has been previously loaded."""
if self.current_url: if self.current_url:
@ -428,15 +445,6 @@ class Browser:
save_bookmark(self.current_url, title) save_bookmark(self.current_url, title)
self.reset_status() self.reset_status()
def open_external_program(self, command):
"""Pauses the curses modes to open an external program."""
curses.nocbreak()
curses.echo()
subprocess.run(command)
curses.noecho()
curses.cbreak()
self.refresh_windows()
def edit_page(self): def edit_page(self):
"""Open a text editor to edit the page source. """Open a text editor to edit the page source.
@ -463,6 +471,7 @@ class Browser:
delete_source_after = True delete_source_after = True
command.append(source_filename) command.append(source_filename)
self.open_external_program(command) open_external_program(command)
if delete_source_after: if delete_source_after:
os.unlink(source_filename) os.unlink(source_filename)
self.refresh_windows()

View file

@ -3,12 +3,21 @@
import curses import curses
import curses.ascii import curses.ascii
import curses.textpad import curses.textpad
import os
import tempfile
from bebop.external import open_external_program
from bebop.links import Links from bebop.links import Links
class CommandLine: class CommandLine:
"""Basic and flaky command-line à la Vim, using curses module's Textbox.""" """Basic and flaky command-line à la Vim, using curses module's Textbox.
I don't understand how to get proper pad-like behaviour, e.g. to scroll past
the window's right border when writing more content than the width allows.
Therefore I just added the M-e keybind to call an external editor and use
its content as result.
"""
def __init__(self, window): def __init__(self, window):
self.window = window self.window = window
@ -78,6 +87,9 @@ class CommandLine:
ch = self.window.getch() ch = self.window.getch()
if ch == -1: if ch == -1:
raise EscapeCommandInterrupt() raise EscapeCommandInterrupt()
else: # ALT keybinds.
if ch == ord("e"):
self.open_editor(self.gather())
self.window.nodelay(False) self.window.nodelay(False)
return ch return ch
@ -151,6 +163,27 @@ class CommandLine:
# Everything else could be a control character and should be processed. # Everything else could be a control character and should be processed.
return ch return ch
def open_editor(self, existing_content=None):
"""Open an external editor and raise termination interrupt."""
try:
with tempfile.NamedTemporaryFile("w+t", delete=False) as temp_file:
if existing_content:
temp_file.write(existing_content)
temp_filepath = temp_file.name
except OSError:
return
command = ["vi", temp_filepath]
open_external_program(command)
try:
with open(temp_filepath, "rt") as temp_file:
content = temp_file.read()
os.unlink(temp_filepath)
except OSError:
return
raise TerminateCommandInterrupt(content)
class EscapeCommandInterrupt(Exception): class EscapeCommandInterrupt(Exception):
"""Signal that ESC has been pressed during command line.""" """Signal that ESC has been pressed during command line."""

17
bebop/external.py Normal file
View file

@ -0,0 +1,17 @@
"""Call external commands."""
import curses
import subprocess
def open_external_program(command):
"""Call command as a subprocess, suspending curses rendering.
The caller has to refresh whatever windows it manages after calling this
method or garbage may be left on the screen.
"""
curses.nocbreak()
curses.echo()
subprocess.run(command)
curses.noecho()
curses.cbreak()

View file

@ -1,7 +1,5 @@
"""Links manager.""" """Links manager."""
from typing import List
class Links(dict): class Links(dict):

View file

@ -53,11 +53,11 @@ def set_parameter(url: str, user_input: str):
def get_parent_url(url: str) -> str: def get_parent_url(url: str) -> str:
"""Return the parent URL (one level up).""" """Return the parent URL (one level up)."""
scheme, netloc, path, params, query, frag = parse_url(url) scheme, netloc, path, _, _, _ = parse_url(url)
last_slash = path.rstrip("/").rfind("/") last_slash = path.rstrip("/").rfind("/")
if last_slash > -1: if last_slash > -1:
path = path[:last_slash + 1] path = path[:last_slash + 1]
return urllib.parse.urlunparse((scheme, netloc, path, params, query, frag)) return urllib.parse.urlunparse((scheme, netloc, path, "", "", ""))
def get_root_url(url: str) -> str: def get_root_url(url: str) -> str:

View file

@ -1,6 +1,6 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from bebop.gemtext import parse_gemtext, Title from bebop.gemtext import parse_gemtext
from bebop.metalines import generate_dumb_metalines, generate_metalines from bebop.metalines import generate_dumb_metalines, generate_metalines
from bebop.links import Links from bebop.links import Links

View file

@ -98,3 +98,10 @@ class PagePad:
self.current_line = max_line self.current_line = max_line
return True return True
return False return False
def go_to_first_column(self):
"""Move to the first column; return True if a refresh is needed."""
if self.current_column:
self.current_column = 0
return True
return False