This question used to contain a mistake in the code, so I had to debug and reformat it. This is the correct version.
Why I made it
A friend and I wanted to make a lightweight browser, since our Raspberry Pi Zero 2 W couldn’t handle popular browsers like Chromium. We also wanted to bring back our own version of Clippit, aka Clippy, for those who remember the annoying helper in Microsoft Word from the early 2000s. We named our version Mano, and the browser itself is titled ManoSearch.
What it does
When you run the script, an ASCII window fills your screen with my friend’s version of Clippy in the top-right corner. You can:
- Open URLs by entering
+then the URL you want. - Navigate tabs on top of the screen by entering the tab number you want.
- Scroll websites with
uandd(you can also get back to the top by usingU) - Open links in new tabs by entering them (links look like
[a b c]and can be entered partially as long as it contains a[or a]) - Closing a tab by entering
x - Enter
videoorvidto useytdlpto download a video from the current website and save it undervid.mp4 - Delete a previousely downloaded video by entering
rm vid - Rename a video to be able to download some more using
mv vid - Find all occurences of something by entering
lookup Find this and highlight it
It does not support javascript or input interaction. To mimic how annoying the original Clippy was, whenever Mano speaks, the current tab’s content disappears. Occasionally, a chicken crosses the browser—you just have to wait for it to cross the screen before you get to continue. And no, you cannot get rid of either of them :)
Problems
Two main issues concern me:
- Sometimes, when a link opens in a new tab, links clicked there search on the original tab instead. It only searches on the last manually opened tab (enter
+), not via link (enter[Link name here]). - I’d love to add javascript support, but I don’t know how.
Code
Here is the now working PEP-8 compliant Python script:
"""
ManoSearch
An ultra-light web browser with the ultra-annoying cousin of Clippy, MANO!
"""
import subprocess
import os
import time
from bs4 import BeautifulSoup
import html2text
import requests
values_list = [] # all opened tabs
currenttab = 0 # currently selected tab
# gethtmlwithjs
import time
import threading
import random
XP = 0
lock = threading.Lock()
content = ""
# Global variable to hold the elapsed time
elapsed_time = 0
# Global variable for when to show the chicken
chickenshow = 300
# Global variable for if the chicken appeared
# Note that if the user is not doing anything
# The chicken will never pass
# (This is most useful for when the egg appears
chickenshowed = False
youshallnotpass = True
chickenegg = random.randint(8, os.get_terminal_size().columns - 8)
chickenegglayer = 0
def draw_frame(blabla):
print("I do nothing but define myself for later on")
# Timer function
def timer():
global content
global elapsed_time
start = time.time()
elapsed_time = 0
while True:
# elapsed_time = int(time.time() - start)
# print(elapsed_time+1)
elapsed_time += 1
time.sleep(0.05)
# Start the timer in a background thread
timer_thread = threading.Thread(target=timer, daemon=True)
timer_thread.start()
def incomplete_brackets(lines):
merged_lines = []
buffer = ""
extra = []
print(lines.splitlines()[1])
time.sleep(0.1)
for i in range(len(lines.splitlines())):
line = lines.splitlines()[i]
if "[" in line and "]" not in line:
buffer = line
print(f"Found buffer {buffer}")
while "]" not in buffer:
i += 1
buffer += " "
buffer += lines.splitlines()[i]
print(f"buffer is now {buffer}")
extra.append(i)
merged_lines.append("\n")
merged_lines.append(buffer)
else:
merged_lines.append("\n")
merged_lines.append(line)
for a in extra:
print(merged_lines[(a * 2) + 1]) # *2 since newlines
merged_lines[(a * 2) + 1] = ""
return "".join(merged_lines)
"""How I used to convert HTML divs into text based on inline styles
soup = BeautifulSoup(html, 'html.parser')
output = [[' ' for _ in range(screen_width)] for _ in range(screen_height)]
def draw_div(node, x, y, width, height):
#Draws a div as ASCII art
if width <= 0 or height <= 0 or x + width >= screen_width or y + height >= screen_height:
return # Ignore divs that don't fit
# Draw borders
output[y][x] = '+'
output[y][x + width - 1] = '+'
output[y + height - 1][x] = '+'
output[y + height - 1][x + width - 1] = '+'
for i in range(1, width - 1):
output[y][x + i] = '-'
output[y + height - 1][x + i] = '-'
for i in range(1, height - 1):
output[y + i][x] = '|'
output[y + i][x + width - 1] = '|'
# Fill content
content_y = y + 1
for line in node.stripped_strings:
if content_y < y + height - 1:
line_content = line[:width - 2] # Leave space for borders
output[content_y][x + 1:x + 1 + len(line_content)] = line_content
content_y += 1
y_offset = 1
for div in soup.find_all("div"):
style = div.get('style', '')
width, height = parse_style(style)
draw_div(div, 1, y_offset, width, height)
y_offset += height + 1
return '\n'.join(''.join(row) for row in output)
"""
height = os.get_terminal_size().lines - 1
width = os.get_terminal_size().columns
def draw_frame(content, hat=0):
global chickenshow
global chickenegglayer
global chickenshowed
global youshallnotpass
global XP
drewegg = False
os.system("clear" if os.name == "posix" else "cls")
width = os.get_terminal_size().columns
# Set outside because needed for scrolling later on
# Set inside in case the screen changes during run time
height = os.get_terminal_size().lines - 1
print(f"+" + "-" * (width - 2) + "+", end="\n")
widthleft = width
print(f"|", end="")
for i in range(1, len(values_list) + 1):
if i == currenttab:
print(f"<{i}>|", end="")
else:
print(f" {i} |", end="")
widthleft -= 4
print(f" (+) ", end="")
widthleft -= 5
print(" " * (widthleft - 2) + "|")
print("|" + "-" * (width - 2) + "|")
content_lines = content.splitlines()
# xpbef = XP
for i in range(height - 10):
if i < len(content_lines):
# This cuts off excess from lines to get the frame right, but I should find a way to send them to the next line instead because some screens are too small to fit it all...
# 1 is witch
# 2 is batman
# 3 is bart.s
if hat == 1 and i == 1:
print(
"|" + content_lines[i][: width - 2].ljust(width - 9) + " _/-\\_ |"
)
elif hat == 2 and i == 1:
print("|" + content_lines[i][: width - 2].ljust(width - 9) + " /\ /\ |")
elif i == 1:
print("|" + content_lines[i][: width - 2].ljust(width - 9) + " ___ |")
elif i == 2:
print("|" + content_lines[i][: width - 2].ljust(width - 9) + " (o-o) |")
elif i == 3 and hat == 1:
print(
"|" + content_lines[i][: width - 2].ljust(width - 9) + "0(/ \\)0|"
)
elif i == 3:
print("|" + content_lines[i][: width - 2].ljust(width - 9) + "0(i i)0|")
elif i == 4:
print("|" + content_lines[i][: width - 2].ljust(width - 9) + " 0-0 |")
elif (
i == 5
and elapsed_time > chickenshow
and elapsed_time < chickenshow + width - 10
and youshallnotpass == False
):
print(
"|"
+ content_lines[i][: width - 2].ljust(
width - (11 + (elapsed_time - chickenshow))
)
+ " ___ "
+ (elapsed_time - chickenshow) * " "
+ "|"
)
elif (
i == 6
and elapsed_time > chickenshow
and elapsed_time < chickenshow + width - 10
and youshallnotpass == False
):
print(
"|"
+ content_lines[i][: width - 2].ljust(
width - (11 + (elapsed_time - chickenshow))
)
+ " <(^ ) "
+ (elapsed_time - chickenshow) * " "
+ "|"
)
elif (
i == 7
and elapsed_time > chickenshow
and elapsed_time < chickenshow + width - 10
and youshallnotpass == False
):
print(
"|"
+ content_lines[i][: width - 2].ljust(
width - (11 + (elapsed_time - chickenshow))
)
+ " ( D )k"
+ (elapsed_time - chickenshow) * " "
+ "|"
)
elif (
i == 8
and elapsed_time > chickenshow
and elapsed_time < chickenshow + width - 10
and youshallnotpass == False
):
# speedy legs :)
print(
"|"
+ content_lines[i][: width - 2].ljust(
width - (11 + (elapsed_time - chickenshow))
)
+ " | | "
+ (
(elapsed_time - chickenshow) * " "
if chickenegg > elapsed_time - chickenshow
else (abs(chickenegg - (elapsed_time - chickenshow)) * " ")
+ (
"🥚"
if elapsed_time - chickenshow < chickenegg + 3
else "+10"
)
+ (
(
abs(
width
- (
width
- (
chickenegg
- (
1
if elapsed_time - chickenshow
< chickenegg + 3
else 3
)
)
)
)
* " "
)
)
)
+ "|"
)
if chickenegg < elapsed_time - chickenshow:
chickenshowed = True
elif height - 11 == i: # does not work
print(
"|"
+ content_lines[i][: width - 2].ljust(width - 11)
+ "XP:"
+ str(XP)
)
else:
print("|" + content_lines[i][: width - 2].ljust(width - 2) + "|")
else:
print("|" + " " * (width - 2) + "|")
drewegg = False
print("+" + "-" * (width - 2) + "+")
print(
"\nEnter command (X to exit, + to fetch URL, x to close tab, ? for help): ",
end="",
)
def openvid(url):
os.system(f'yt-dlp -F "{url}"')
port = input("Enter chosen port:")
os.system(f'yt-dlp --ignore-errors -f {port} -o "vid.mp4" "{url}"')
os.system("open vid.mp4")
time.sleep(5)
# os.system('rm video.mp4')
def fetch_website(url):
try:
# result = subprocess.run(["curl", "-L", "-s", url], capture_output=True, text=True)
result = requests.get(url)
return result.text
# if result.returncode == 0:
# return result.stdout
# else:
# return f"Error fetching URL:\n\n{url}\n(Error code: {result.returncode})"
except Exception as e:
return str(e)
def add_to_list(value):
values_list.append(value)
def main():
global currenttab
global elapsed_time
global content
content = "Welcome to Mano's web browser!".replace("\n", "")
content = (
((width - 12) - (len(content) + 2)) * " "
+ (len(content) + 2) * "-"
+ "\n"
+ ((width - 12) - (len(content) + 3)) * " "
+ "( "
+ content
+ " )\n"
+ ((width - 12) - (len(content) + 2)) * " "
+ (len(content) + 3) * "-"
+ "\n\n\n\n\n\n\n\n\n\n"
)
draw_frame(content, 0)
scroll = 0
lastup = False
lastdown = False
global chickenshow
global chickenshowed
global XP
global chickenegg
global youshallnotpass
url = "https://theuselessweb.com/"
URL = ""
findthis = "SomeRandomTextThatWillNeverBeFound"
while True:
if elapsed_time <= chickenshow and elapsed_time >= chickenshow - 15:
youshallnotpass = False
chickenshowed = False
while elapsed_time < chickenshow:
time.sleep(0.1)
while elapsed_time <= chickenshow + width - 10:
draw_frame(content + "\n\n\n\n\n\n\n\n\n\n", 1)
time.sleep(0.1)
elapsed_time = 0
chickenegg = random.randint(7, os.get_terminal_size().columns - 7)
XP += 10
else:
youshallnotpass = True
# Take that, you sneaky chicken!
# Stop roamin when you shouldn't!
if elapsed_time > chickenshow:
elapsed_time = 0
before = elapsed_time
command = input().strip()
if (
elapsed_time >= chickenshow
and before < chickenshow
and chickenshowed == False
):
elapsed_time = before - 10
# elapsed_time = 0
if command == "X":
break
elif command == "x":
if currenttab >= 1:
values_list.remove(values_list[currenttab - 1])
currenttab -= 1
if currenttab == 0:
try:
content = values_list[0]
currenttab = 1
except IndexError:
content = "Closed all tabs!"
content = (
((width - 12) - (len(content) + 2)) * " "
+ (len(content) + 2) * "-"
+ "\n"
+ ((width - 12) - (len(content) + 3)) * " "
+ "( "
+ content
+ " )\n"
+ ((width - 12) - (len(content) + 2)) * " "
+ (len(content) + 3) * "-"
+ "\n\n\n\n\n\n\n\n\n\n"
)
draw_frame(content, 0)
time.sleep(1)
content = "Welcome back to Mano's web browser!"
content = (
((width - 12) - (len(content) + 2)) * " "
+ (len(content) + 2) * "-"
+ "\n"
+ ((width - 12) - (len(content) + 3)) * " "
+ "( "
+ content
+ " )\n"
+ ((width - 12) - (len(content) + 2)) * " "
+ (len(content) + 3) * "-"
+ "\n\n\n\n\n\n\n\n\n\n"
)
else:
content = values_list[currenttab - 1]
else:
content = "No tabs opened!"
draw_frame(content, 0)
time.sleep(1)
content = "Welcome back"
elif command == "+":
url = input("Enter URL: ")
content = fetch_website(url)
print("Loading...")
text_maker = html2text.HTML2Text()
# text_maker.ignore_links = True
ascii_view = text_maker.handle(content)
# Took off ' "' -> ')\n"'
content = (
ascii_view.replace(")[", ")\n[")
.replace("]", "]\n")
.replace(" [", "\n[")
.replace(") |", ")")
)
content = incomplete_brackets(content)
# replace(' | [', '\n[')
add_to_list(content)
currenttab = len(values_list)
elif "search " in command:
if not command.replace("search ", "") == "":
url = "https://yandex.com/video/touch/search?utm_source=yandex&utm_medium=com&utm_campaign=morda&text=" + command.replace(
"search ", ""
).replace(
" ", "-"
)
# url = "https://wiby.me/?q="+command.replace('search ', '').replace(' ', '+')
# url = "https://marginalia-search.com/search?query="+command.replace('search ','').replace(' ','+')
# url = "https://www.metacrawler.com/serp?qc=web&q="+command.replace('search ','').replace(' ', '+')+"&sc=WcX3wibC4yWx10"
print(url)
result = subprocess.run(
["curl", "-L", "-s", url], capture_output=True, text=True
)
if result.returncode == 0:
content = result.stdout
else:
content = fetch_website(url)
text_maker = html2text.HTML2Text()
ascii_view = text_maker.handle(content)
# Took off ' "' -> ')\n"'
content = (
ascii_view.replace(")[", ")\n[")
.replace("]", "]\n")
.replace(" [", "\n[")
.replace(") |", ")")
.replace(' "', ')\n"')
)
content = incomplete_brackets(content)
add_to_list(content)
currenttab = len(values_list)
else:
before = content
content = "No search content detected"
content = (
((width - 12) - (len(content) + 2)) * " "
+ (len(content) + 2) * "-"
+ "\n"
+ ((width - 12) - (len(content) + 3)) * " "
+ "( "
+ content
+ " )\n"
+ ((width - 12) - (len(content) + 2)) * " "
+ (len(content) + 3) * "-"
+ "\n\n\n\n\n\n\n\n\n\n"
)
draw_frame(content, 0)
time.sleep(5)
content = before
elif "video" == command:
openvid(url)
elif "rm vid" in command or "remove vid" in command:
os.system("rm vid.mp4")
elif "save vid" in command or "mv vid" in command or "move vid" in command:
vidnum = (
input("Enter desired saved video name without suffix: ")
.replace(" ", "-")
.replace(".", "_")
)
os.system(f"touch {vidnum}.mp4")
os.system(f"mv vid.mp4 {vidnum}.mp4")
before = content
content = f"Most recent video got moved to destination {vidnum}.mp4."
content = (
((width - 12) - (len(content) + 2)) * " "
+ (len(content) + 2) * "-"
+ "\n"
+ ((width - 12) - (len(content) + 3)) * " "
+ "( "
+ content
+ " )\n"
+ ((width - 12) - (len(content) + 2)) * " "
+ (len(content) + 3) * "-"
+ "\n\n\n\n\n\n\n\n\n\n"
)
draw_frame(content, 0)
time.sleep(4)
content = "You may now download other videos!"
content = (
((width - 12) - (len(content) + 2)) * " "
+ (len(content) + 2) * "-"
+ "\n"
+ ((width - 12) - (len(content) + 3)) * " "
+ "( "
+ content
+ " )\n"
+ ((width - 12) - (len(content) + 2)) * " "
+ (len(content) + 3) * "-"
+ "\n\n\n\n\n\n\n\n\n\n"
)
draw_frame(content, 0)
time.sleep(2.7)
content = before
elif command == "?":
before = content
content = (
"Commands: (TIP:ENTER ? MANY TIMES BEFORE THIS CLOSES)\n"
"+ : Fetch and display a website\n"
"? : Show this help screen\n"
"X : Exit the program\n"
"x : Exit the current tab\n"
"# : Switch to another tab (by number)\n"
"[example] : Open link in a new tab\n(Enter help[] for more details on how to use)\n"
"up (u) or down (d) : Scroll web content in entered direction\n"
"U : Scroll back to top\n"
"video : Try to extract and download the main video from the current website (and ask to open)\n"
"rm video : Shortcut to remove the video (nothing happens if no video was installed)\n"
"url : (was for dev purposes) Show last opened url and ask to open in default web browser\n"
"? : No need to wonder what this does :)"
)
draw_frame(content, 0)
time.sleep(5)
content = before
elif command == "help[]":
before = content
content = (
"About links:\n"
"To open a link in a new tab, first locate the link. \nIt should look like this:\n"
"[Example]\n"
"Enter the link as it is,\nincluding the capital\nletters and the brackets,\neven if one is missing.\n"
"IMPORTANT:\nIf the link spans over \nmultiple newlines, like this:\n[This\nis an\nexample]\n, then make sure to only \nenter the last line as it is.\n(In this case, 'example]' should be entered.)\nETA20s\n"
)
elif command == "url":
print(url)
openornot = input("Open url? Y/N : ")
if openornot == "Y" or openornot == "y":
os.system(f"open {url}")
time.sleep(2)
elif "lookup " in command:
command = command.replace("lookup ", "")
findthis = command
before = content
content = f"All occurences of {findthis} are now obvious."
draw_frame(content, 0)
time.sleep(3)
content = before
elif command == "" or command == "0" or command == "\n" or command == " ":
if lastup:
command = "up" # to keep going
if scroll > 1:
scroll -= 2
else:
before = content
content = "Already at the top!"
content = (
((width - 12) - (len(content) + 2)) * " "
+ (len(content) + 2) * "-"
+ "\n"
+ ((width - 12) - (len(content) + 3)) * " "
+ "( "
+ content
+ " )\n"
+ ((width - 12) - (len(content) + 2)) * " "
+ (len(content) + 3) * "-"
+ "\n\n\n\n\n\n\n\n\n\n\n"
)
draw_frame(content, 0)
time.sleep(0.5) # If spammed wait a little less long
content = before
elif lastdown:
command = "down" # to keep going
scroll += 2
else:
before = content
content = "Nothing has been entered."
content = (
((width - 12) - (len(content) + 2)) * " "
+ (len(content) + 2) * "-"
+ "\n"
+ ((width - 12) - (len(content) + 3)) * " "
+ "( "
+ content
+ " )\n"
+ ((width - 12) - (len(content) + 2)) * " "
+ (len(content) + 3) * "-"
+ "\n\n\n\n\n\n\n\n\n\n"
)
draw_frame(content, 0)
# draw_frame(before)
time.sleep(0.7)
content = before
elif command == "up" or command == "u":
if scroll > 1:
scroll -= 2
else:
before = content
content = "Already at the top!"
content = (
((width - 12) - (len(content) + 2)) * " "
+ (len(content) + 2) * "-"
+ "\n"
+ ((width - 12) - (len(content) + 3)) * " "
+ "( "
+ content
+ " )\n"
+ ((width - 12) - (len(content) + 2)) * " "
+ (len(content) + 3) * "-"
+ "\n\n\n\n\n\n\n\n\n\n"
)
draw_frame(content, 0)
time.sleep(0.5)
content = before
elif command == "down" or command == "d":
scroll += 2
elif command.lower() == "up" or command.lower() == "u":
scroll = 0
elif "[" in command or "]" in command:
input_string = content
for i in range(len(input_string.splitlines())): # Find the link (URL)
if command in input_string.splitlines()[i]:
URL = input_string.splitlines()[i + 1]
# Experimental. sometimes the link is across multiple lines.
counter = 1
while (len(input_string.splitlines()) - i) > counter:
counter += 1
if ")" not in URL:
URL += input_string.splitlines()[i + counter]
URL = URL.replace("\n", "")
else:
break
if "http://" not in URL and "https://" not in URL: # and 'www' not in URL:
# Some local links bring you to a different subfolder of the current website
# For example, "/index.html"
# This if statement binds both the current website URL (url) and the destination URL (URL)
# Since you cant just request "/index.html" as it is
# Three valid examples:
print(url) # https://google.com https://yahoo.org/ http://uh.com/oh
print(URL) # (/search/) (/about-us) (um)
time.sleep(0.05)
url = url + " "
url = url.replace("/ ", "").replace(" ", "")
print(url) # https://google.com https://yahoo.org http://uh.com/oh
print(URL) # (/search/) (/about-us) (um)
time.sleep(0.05)
URL = URL.replace("(", "").replace(")", "").replace(" ", "")
print(url) # https://google.com https://yahoo.org http://uh.com/oh
print(URL) # /search/ /about-us um
time.sleep(0.05)
URL = " " + URL
URL = URL.replace(" /", "/").replace(" ", "/")
print(url) # https://google.com https://yahoo.org http://uh.com/oh
print(URL) # /search/ /about-us /um
time.sleep(0.05)
if url.startswith("https://"):
url = url.replace("https://", "")
atfirstslash = False
newurl = []
for a in url:
if atfirstslash == False:
if a == "/":
atfirstslash = True
else:
newurl.append(a)
url = "https://" + "".join(newurl)
print(url)
time.sleep(0.05)
elif url.startswith("http://"):
url = url.replace("http://", "")
atfirstslash = False
newurl = []
for a in url:
if atfirstslash == False:
if a == "/":
atfirstslash = True
else:
newurl.append(a)
url = "http://" + "".join(newurl)
print(url)
time.sleep(0.05)
print(f"Finished binding: {url}{URL}")
if 1 == 2: # not url.startswith("http"):
print(f"Uh oh! HTTP not found in url!")
url = "https:" + url
if "https://" not in url:
url = "https://" + url.replace("https:", "")
print(f"url: {url}")
time.sleep(0.05)
URL = URL.replace(url.replace("https:", "").replace("http:", ""), "")
print(f"\n\n\nOpening {url}{URL}")
time.sleep(0.1)
url = url + URL # For further link pressing, full link is needed
content = fetch_website(url)
else:
url = URL.replace("(", "").replace(")", "").replace(" ", "")
content = fetch_website(url)
text_maker = html2text.HTML2Text()
ascii_view = text_maker.handle(content)
content = (
ascii_view.replace(")[", ")\n[")
.replace("]", "]\n")
.replace(' "', ')\n"')
.replace(" [", "\n[")
.replace(") |", ")")
)
content = incomplete_brackets(content)
add_to_list(content)
currenttab = len(values_list)
else:
previoustab = currenttab
try:
content = values_list[int(command) - 1]
currenttab = int(command)
except (IndexError, ValueError):
before = content
content = "Invalid command.\nFor help, enter ?"
content = (
((width - 12) - (len(content) + 2)) * " "
+ (len(content) + 2) * "-"
+ "\n"
+ ((width - 12) - (len(content) + 3)) * " "
+ "( "
+ content
+ " )\n"
+ ((width - 12) - (len(content) + 2)) * " "
+ (len(content) + 3) * "-"
+ "\n\n\n\n\n\n\n\n\n\n"
)
draw_frame(content, 0)
time.sleep(2)
content = before
currenttab = previoustab
if command == "up" or command == "u":
lastup = True
else:
lastup = False
if command == "down" or command == "d":
lastdown = True
else:
lastdown = False
before = content
# Also removes those lines I get simetimes that have no more than a single char, like [ or ]
# Took off [ or ] -----------
content = (
"\n".join(
line
for line in content.splitlines()
if not line.startswith("(/")
and not line.startswith("(../")
and not line.startswith("(http")
and not line == "["
and not line == "]"
and not line == " "
)
+ "\n \n \n \n**END**"
)
# dunno exactly how come my biggest one-liner ever works but it got scrolling right so...
draw_frame(
(
"\n".join(
(lines := content.split("\n"))[
max(0, min(scroll, max(0, len(lines) - (height - 3)))) : max(
0, min(scroll, max(0, len(lines) - (height - 3)))
)
+ (height - 3)
]
)
).replace(findthis, "<<<<" + findthis + ">>>>")
)
content = before
if __name__ == "__main__":
main()

