1
\$\begingroup\$

Note that this is the original version which has not been formatted to comply to PEP-8 and contains an error in the code. Please visit the updated version instead to post answers, comments and/or cast your votes.

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.

Mano!

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:

  1. Open URLs by entering + then the URL you want.
  2. Navigate tabs on top of the screen by entering the tab number you want.
  3. Scroll websites with u and d (you can also get back to the top by using U)
  4. 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 ])
  5. Closing a tab by entering x
  6. Enter video or vid to use ytdlp to download a video from the current website and save it under vid.mp4
  7. Delete a previousely downloaded video by entering rm vid
  8. Rename a video to be able to download some more using mv vid
  9. 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

Three main issues concern me:

  1. My code is a bit sloppy, I'm still relatively new to PEP rules, so if you spot anything that could be better please tell me in the comments.
  2. 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]).
  3. I’d love to add javascript support, but I don’t know how.

Code

Here is the Python script:

"""
ManoSearch
An ultra-light web browser with the ultra-annoying cousin of Clippy, MANO!
"""
import os
import time
import random
import threading
import subprocess
import requests
from bs4 import BeautifulSoup
import html2text

# Global Variables
values_list = []  # All opened tabs
current_tab = 0  # Currently selected tab
XP = 0
lock = threading.Lock()
content = ""
elapsed_time = 0  # Global variable to hold the elapsed time
chicken_show_time = 300  # Global variable for when to show the chicken
chicken_showed = False  # If the chicken has appeared
you_shall_not_pass = True
chicken_egg = random.randint(8, os.get_terminal_size().columns - 8)
chicken_egg_layer = 0


def draw_frame(content, hat=0):
    """Draws the frame around the content."""
    global chicken_show_time
    global chicken_egg_layer
    global chicken_showed
    global you_shall_not_pass
    global XP

    drewegg = False
    os.system('clear' if os.name == 'posix' else 'cls')

    width = os.get_terminal_size().columns
    height = os.get_terminal_size().lines - 1

    print(f"+" + "-" * (width - 2) + "+", end="\n")
    width_left = width
    print(f"|", end="")

    for i in range(1, len(values_list) + 1):
        if i == current_tab:
            print(f"<{i}>|", end="")
        else:
            print(f" {i} |", end="")
        width_left -= 4
    print(f" (+) ", end="")
    width_left -= 5
    print(" " * (width_left - 2) + "|")
    print("|" + "-" * (width - 2) + "|")

    content_lines = content.splitlines()

    for i in range(height - 10):
        if i < len(content_lines):
            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 > chicken_show_time and elapsed_time < chicken_show_time + width - 10 and not you_shall_not_pass:
                print("|" + content_lines[i][:width - 2].ljust(width - (11 + (elapsed_time - chicken_show_time))) + "   ___   " + (elapsed_time - chicken_show_time) * " " + "|")
            elif i == 6 and elapsed_time > chicken_show_time and elapsed_time < chicken_show_time + width - 10 and not you_shall_not_pass:
                print("|" + content_lines[i][:width - 2].ljust(width - (11 + (elapsed_time - chicken_show_time))) + " <(^  )  " + (elapsed_time - chicken_show_time) * " " + "|")
            elif i == 7 and elapsed_time > chicken_show_time and elapsed_time < chicken_show_time + width - 10 and not you_shall_not_pass:
                print("|" + content_lines[i][:width - 2].ljust(width - (11 + (elapsed_time - chicken_show_time))) + "   ( D )k" + (elapsed_time - chicken_show_time) * " " + "|")
            elif i == 8 and elapsed_time > chicken_show_time and elapsed_time < chicken_show_time + width - 10 and not you_shall_not_pass:
                print("|" + content_lines[i][:width - 2].ljust(width - (11 + (elapsed_time - chicken_show_time))) + "    |  | " + ((elapsed_time - chicken_show_time) * " " if chicken_egg > elapsed_time - chicken_show_time else (abs(chicken_egg - (elapsed_time - chicken_show_time)) * " ") + ("🥚" if elapsed_time - chicken_show_time < chicken_egg + 3 else "+10") + ((abs(width - (width - (chicken_egg - (1 if elapsed_time - chicken_show_time < chicken_egg + 3 else 3)))) * " "))) + "|")
                if chicken_egg < elapsed_time - chicken_show_time:
                    chicken_showed = True
            elif height - 11 == i:
                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 timer():
    """Runs a background timer."""
    global content
    global elapsed_time
    start = time.time()
    elapsed_time = 0
    while True:
        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):
    """Fixes incomplete brackets in the provided lines."""
    merged_lines = []
    buffer = ""
    extra = []

    for i in range(len(lines.splitlines())):
        line = lines.splitlines()[i]
        if "[" in line and "]" not in line:
            buffer = line
            while "]" not in buffer:
                i += 1
                buffer += " " + lines.splitlines()[i]
                extra.append(i)
            merged_lines.append("\n")
            merged_lines.append(buffer)
        else:
            merged_lines.append("\n")
            merged_lines.append(line)

    for a in extra:
        merged_lines[(a * 2) + 1] = ""
    return ''.join(merged_lines)


def fetch_website(url):
    """Fetches the website content."""
    try:
        result = requests.get(url)
        return result.text
    except Exception as e:
        return str(e)


def add_to_list(value):
    """Adds a value to the list of open tabs."""
    values_list.append(value)


def open_vid(url):
    """Fetches and opens a video using yt-dlp."""
    os.system(f'yt-dlp -F "{url}" --age-limit 99')
    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)


def main():
    """Main function that drives the application."""
    global current_tab
    global elapsed_time
    global content

    content = "Welcome to Mano's web browser!".replace('\n', '')
    content = ((os.get_terminal_size().columns - 12) - (len(content) + 2)) * " " + (len(content) + 2) * "-" + "\n" + ((os.get_terminal_size().columns - 12) - (len(content) + 3)) * " " + "( " + content + " )\n" + ((os.get_terminal_size().columns - 12) - (len(content) + 2)) * " " + (len(content) + 3) * "-" + "\n\n\n\n\n\n\n\n\n\n"
    draw_frame(content, 0)

    scroll = 0
    last_up = False
    last_down = False
    global chicken_show_time
    global chicken_showed
    global XP
    global chicken_egg
    global you_shall_not_pass

    url = "https://theuselessweb.com/"
    while True:
        if elapsed_time <= chicken_show_time and elapsed_time >= chicken_show_time - 15:
            you_shall_not_pass = False
            chicken_showed = False
            while elapsed_time < chicken_show_time:
                time.sleep(0.1)
            while elapsed_time <= chicken_show_time + os.get_terminal_size().columns - 10:
                draw_frame(content + "\n\n\n\n\n\n\n\n\n\n", 1)
                time.sleep(0.1)
            elapsed_time = 0
            chicken_egg = random.randint(7, os.get_terminal_size().columns - 7)
            XP += 10
        else:
            you_shall_not_pass = True

        if elapsed_time > chicken_show_time:
            elapsed_time = 0

        before = elapsed_time
        command = input().strip()

        if elapsed_time >= chicken_show_time and before < chicken_show_time and not chicken_showed:
            elapsed_time = before - 10

        if command == 'X':
            break
        elif command == 'x':
            if current_tab >= 1:
                values_list.remove(values_list[current_tab - 1])
                current_tab -= 1
                if current_tab == 0:
                    try:
                        content = values_list[0]
                        current_tab = 1
                    except IndexError:
                        content = "Closed all tabs!"
                        content = ((os.get_terminal_size().columns - 12) - (len(content) + 2)) * " " + (len(content) + 2) * "-" + "\n" + ((os.get_terminal_size().columns - 12) - (len(content) + 3)) * " " + "( " + content + " )\n" + ((os.get_terminal_size().columns - 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 = ((os.get_terminal_size().columns - 12) - (len(content) + 2)) * " " + (len(content) + 2) * "-" + "\n" + ((os.get_terminal_size().columns - 12) - (len(content) + 3)) * " " + "( " + content + " )\n" + ((os.get_terminal_size().columns - 12) - (len(content) + 2)) * " " + (len(content) + 3) * "-" + "\n\n\n\n\n\n\n\n\n\n"
                else:
                    content = values_list[current_tab - 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()
            ascii_view = text_maker.handle(content)
            content = ascii_view.replace(')[', ')\n[').replace(']', ']\n').replace(' [', '\n[').replace(') |', ')')
            content = incomplete_brackets(content)
            add_to_list(content)
            current_tab = len(values_list)
        elif 'video' == command:
            open_vid(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:
            vid_num = input("Enter desired saved video name without suffix: ").replace(' ', '-').replace('.', '_')
            os.system(f'touch {vid_num}.mp4')
            os.system(f'mv vid.mp4 {vid_num}.mp4')
            before = content
            content = f"Most recent video got moved to destination {vid_num}.mp4."
            content = ((os.get_terminal_size().columns - 12) - (len(content) + 2)) * " " + (len(content) + 2) * "-" + "\n" + ((os.get_terminal_size().columns - 12) - (len(content) + 3)) * " " + "( " + content + " )\n" + ((os.get_terminal_size().columns - 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 = ((os.get_terminal_size().columns - 12) - (len(content) + 2)) * " " + (len(content) + 2) * "-" + "\n" + ((os.get_terminal_size().columns - 12) - (len(content) + 3)) * " " + "( " + content + " )\n" + ((os.get_terminal_size().columns - 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)
            open_or_not = input("Open url? Y/N : ")
            if open_or_not == "Y" or open_or_not == "y":
                os.system(f'open {url}')
            time.sleep(2)
        elif 'lookup ' in command:
            command = command.replace('lookup ', '')
            find_this = command
            before = content
            content = f"All occurrences of {find_this} are now obvious."
            draw_frame(content, 0)
            time.sleep(3)
            content = before
        elif command == '' or command == '0' or command == '\n' or command == ' ':
            if last_up:
                command = 'up'  # To keep going
                if scroll > 1:
                    scroll -= 2
                else:
                    before = content
                    content = "Already at the top!"
                    content = ((os.get_terminal_size().columns - 12) - (len(content) + 2)) * " " + (len(content) + 2) * "-" + "\n" + ((os.get_terminal_size().columns - 12) - (len(content) + 3)) * " " + "( " + content + " )\n" + ((os.get_terminal_size().columns - 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)
                    content = before
            elif last_down:
                command = 'down'  # To keep going
                scroll += 2
            else:
                before = content
                content = "Nothing has been entered."
                content = ((os.get_terminal_size().columns - 12) - (len(content) + 2)) * " " + (len(content) + 2) * "-" + "\n" + ((os.get_terminal_size().columns - 12) - (len(content) + 3)) * " " + "( " + content + " )\n" + ((os.get_terminal_size().columns - 12) - (len(content) + 2)) * " " + (len(content) + 3) * "-" + "\n\n\n\n\n\n\n\n\n\n"
                draw_frame(content, 0)
                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 = ((os.get_terminal_size().columns - 12) - (len(content) + 2)) * " " + (len(content) + 2) * "-" + "\n" + ((os.get_terminal_size().columns - 12) - (len(content) + 3)) * " " + "( " + content + " )\n" + ((os.get_terminal_size().columns - 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 URL not in ['http://', 'https://']:
                # 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://', '')
                    at_first_slash = False
                    new_url = []
                    for a in url:
                        if not at_first_slash:
                            if a == "/":
                                at_first_slash = True
                            else:
                                new_url.append(a)
                    url = 'https://' + ''.join(new_url)
                    print(url)
                    time.sleep(0.05)
                elif url.startswith('http://'):
                    url = url.replace('http://', '')
                    at_first_slash = False
                    new_url = []
                    for a in url:
                        if not at_first_slash:
                            if a == "/":
                                at_first_slash = True
                            else:
                                new_url.append(a)
                    url = 'http://' + ''.join(new_url)
                    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(' "', ')')
            content = incomplete_brackets(content)
            add_to_list(content)
            current_tab = len(values_list)
        else:
            previous_tab = current_tab
            try:
                content = values_list[int(command) - 1]
                current_tab = int(command)
            except (IndexError, ValueError):
                before = content
                content = "Invalid command.\nFor help, enter ?"
                content = ((os.get_terminal_size().columns - 12) - (len(content) + 2)) * " " + (len(content) + 2) * "-" + "\n" + ((os.get_terminal_size().columns - 12) - (len(content) + 3)) * " " + "( " + content + " )\n" + ((os.get_terminal_size().columns - 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
                current_tab = previous_tab

        if command == 'up' or command == 'u':
            last_up = True
        else:
            last_up = False
        if command == 'down' or command == 'd':
            last_down = True
        else:
            last_down = False

        before = content
        # Also removes those lines I get sometimes 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**"
        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(find_this, '<<<<' + find_this + '>>>>'))
        content = before


if __name__ == "__main__":
    main()

How did he know...

New contributor
Chip01 is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
\$\endgroup\$
0

2 Answers 2

3
\$\begingroup\$

Layout

There are many really long lines. The black program can be used to automatically reformat the code with more reasonable line lengths.

Tools

You could run code development tools to automatically find some style issues with your code.

ruff identifies unused import lines:

import subprocess
from bs4 import BeautifulSoup

UX

When I run the code, I see a message like this:

 SyntaxWarning: invalid escape sequence '\ '
  print("|" + content_lines[i][: width - 2].ljust(width - 9) + " /\ /\ |")

It is hard to notice because the other output pretty much fills my screen below it. You should try to fix the warning.

Naming

buffer is a vague name for a variable. Also, "buffer" has special coloring (syntax highlighting) when I copy the code into my editor. This indicates that the name is likely used for some other purpose. Regardless, perhaps line_buffer would be a better name.

Simpler

This line:

if command == 'up' or command == 'u':

is simpler as:

if command[0] == 'u':

Similar for "down".

DRY

In this line:

content = ((os.get_terminal_size().columns - 12) - (len(content) + 2)) * " " + (len(content) + 2) * "-" + "\n" + ((os.get_terminal_size().columns - 12) - (len(content) + 3)) * " " + "( " + content + " )\n" + ((os.get_terminal_size().columns - 12) - (len(content) + 2)) * " " + (len(content) + 3) * "-" + "\n\n\n\n\n\n\n\n\n\n"

the expression len(content) is used multiple times. You can assign it to a variable and use that instead. This will also be more efficient since you only need to execute len once.

The same goes for int(command).

And width - 2.

And width - 9.

Lots to DRY up.

Partitioning

The main function while loop is very long. Try to partition some of that code out into functions.

\$\endgroup\$
2
  • \$\begingroup\$ It should now work and comply more to PEP-8: The updated version \$\endgroup\$ Commented yesterday
  • 1
    \$\begingroup\$ I would use if command in ['u', 'up'] instead of just checking the first letter. What if a command user is added later? \$\endgroup\$ Commented 10 hours ago
1
\$\begingroup\$

At first glance:

  • I see a lot of global variables.
  • main is too long and complex.
  • Statements like the following can just be expressions.
        if command == 'up' or command == 'u':
            last_up = True
        else:
            last_up = False

Becomes:

        last_up = command == 'up' or command == 'u'

Which could be further reduced to:

        last_up = command in ('up', 'u')
  • There are a number of complex string concatenations that could be better as f-strings.
\$\endgroup\$
1
  • \$\begingroup\$ It should now work and comply more to PEP-8: The updated version \$\endgroup\$ Commented yesterday

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.