6
\$\begingroup\$

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.

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

Two main issues concern me:

  1. 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]).
  2. 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()

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\$

3 Answers 3

6
\$\begingroup\$

Here's a few of the things I noticed while reading through


DRY (Don't Repeat Yourself)

The following repeats itself multiple times, and like toolic stated parts of it repeat as well

    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"
    )

I would probably make this into a function, like

def format_content(content):
    num_char = len(content)
    offset = 12
    return (
        ((width - offset) - (num_char + 2)) * " "
        + (num_char + 2) * "-"
        + "\n"
        + ((width - offset) - (num_char + 3)) * " "
        + "( "
        + content
        + " )\n"
        + ((width - offset) - (num_char + 2)) * " "
        + (num_char + 3) * "-"
        + "\n\n\n\n\n\n\n\n\n\n"
    )

Then, you have

    content = format_content("Welcome to Mano's web browser!".replace("\n", ""))
    draw_frame(content, 0)

or content = format_content("Welcome back to Mano's web browser!") elsewhere. Additionally, it allows you to adjust offset and not have to change the value in every place it's used.

I also noticed content_lines[i][: width - 2] is repeated 13 separate times, which would be a lot more clear if it was explicitly set as a variable then used each time. When it's repeated like that, whoever is reading the code has to pay more attention to check "is this actually the same expression or was the 7th one different?"


Variable naming

You have variables like url and URL next to each other - preferably variables should be snake_case and also it would be better to disambiguate the two.


Simplify some things

Comparison operators can be chained like so:

i == 5
and elapsed_time > chickenshow
and elapsed_time < chickenshow + width - 10
and youshallnotpass == False

is equivalent to

i == 5
and chickenshow < elapsed_time < chickenshow + width - 10
and not youshallnotpass

and

if elapsed_time <= chickenshow and elapsed_time >= chickenshow - 15:

is equivalent to

if chickenshow >= elapsed_time >= chickenshow - 15:

Checks like

if command == "up" or command == "u":
    lastup = True
else:
    lastup = False
if command == "down" or command == "d":
    lastdown = True
else:
    lastdown = False

are, as Chris noted, the same as simply

lastup = command in {"up", "u"}
lastdown = command in {"down", "d"}

Generally, anywhere you sequentially compare a string to multiple options, you can just check if it's in a list or set.


Misc

  • draw_frame() gets redefined - you shouldn't need this placeholder definition
  • It looks like you left in a stray if 1 == 2: when trying to disable a check - you can also just use False to be more explicit
  • There are some strings like " _/-\\_ |", which you might want to use raw string literals for, since they mean you don't have to escape things like \
  • main() is quite long, as is draw_frame(). I'd say that if you have to do a lot of scrolling in the same function, you should consider trying to simplify things
    • honestly the print() after # speedy legs is just a lot as a single expression, which makes it difficult to understand what it's actually doing
    • perhaps some of the conditionals in main() could be extracted to separate functions, which would help with readability
  • Generally, seeing a (or many) global variable(s) is an indicator to me that something probably can be better
    • One example: currenttab doesn't need to be global, you can just pass it to draw_frame() as an argument
    • instead of defining your own timer function, you could simply use start_time = time.time() and then check against elapsed_time = time.time() - start_time, preferably as a function so you don't have to repeat it. However, this would probably mean you would need to adjust your threshold of timing values, but on the other hand, you could avoid using threading entirely

How to help yourself further

As mentioned, there are a number of tools to help with both formatting and also checking for style and simplification. In my experience, it's far easier to set yourself up with one at the start of a project than it is to go back and fix a laundry list towards the end.

I will note that ruff check --fix --unsafe-fixes is decidedly unsafe for your code, since it will remove all of the print(), but it is marked "unsafe" for a reason. However, you can also ignore individual items it checks for, either per-file with # ruff: noqa: T201 or in a pyproject.toml file for the whole project, or even per-line.

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

I will try not to repeat what has already been said:

Syntax Error

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

PEP-8 Compliance

Since you state that you are PEP-8 compliant, let me mention:

  1. All of your import statements should be at the top of your file. You have some intervening global variable definitions between groups of imports.
  2. You should have a blank line between import statements for Python's standard library imports and related third-party imports.
  3. Write docstrings for all public modules, functions, classes, and methods. Docstrings are not necessary for non-public methods, but you should have a comment that describes what the method does. This comment should appear after the def line.

  4. I doubt that you consider all of the functions defined in your code to be public. Those that have been defined for private use should have names that begin with a leading underscore. Likewise for global variables. This will also limit what names will be imported if somebody does a from your_module import *.

Better Help

You offer:

Enter command (X to exit, + to fetch URL, x to close tab, ? for help):

So I entered:

+ https://google.com

And 'Invalid command' appeared briefly before it disappeared. I had to look at the code to figure out that one has to enter '+' by itself and then a prompt for the URL will be made.

I also foolishly tried to exit by entering 'x' instead of 'X'. Could/should this command be case-insensitive? But looking at the code I see that you have separate logic for when 'x' is entered. But this is not described in your help. Would using a different letter for this command, whatever it is supposed to do, be more user-friendly?

Unable to Fetch Page Due to Missing User-Agent Header.

I received the following error when attempting to fetch a URL:

Not Acceptable!                                                                                                                                                                                                                                                                           
An appropriate representation of the requested resource could not be found
on this server. This error was generated by Mod_Security.

This error was caused by not passing to requests.get a User-Agent header. You could use, for example:

headers = {'User-Agent': 'clippy/0.0.1'}
result = requests.get(url, headers=headers)

Pythonic and Efficient Code

Where you have a statement such as ...

elif command == "" or command == "0" or command == "\n" or command == " ":

... use instead:

elif command in ("", "0", "\n", " "):

You have:

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)

The first thing I noticed is that you repeatedly calculate lines.splitlines() without the value of lines having changed. These are redundant and unnecessary calculations. Instead assign split_lines = lines.splitlines() prior to entering the for loop and reference split_lines wherever you have lines.splitlines().

In the same function you have:

            ...
            while "]" not in buffer:
                i += 1
                ...
                buffer += lines.splitlines()[i]
                ...

So if you do not find the closing "]" bracket you look for it in subsequent lines. But what if there is no closing bracket? Won't i become an invalid index and cause lines.splitlines()[i] to raise an exception? Or am I missing something? If so, include a comments as to why this is correct.

You are also doing potentially a lot of string concatenations to variable buffer. Consider making buffer a list of strings to which you do a final ''.join(buffer) to create the concatenation of the strings.

Improved Timing

You currently have:

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)

Note that start is being assigned but not used (but it could be, see below).

This function, which runs in a separate thread, apparently is trying to increment elapsed_time by 1 every .05 seconds. Since it is competing with other threads for the CPU within the same or different processes, this will not be particularly accurate. You could do better with:

def timer():
    global elapsed_time
    start = time.monotonic()
    elapsed_time = 0

    while True:
        time.sleep(0.05)
        elapsed_time = int((time.monotonic() - start) / .05)

Simplify main

You have a very large and complicated if/elif construct testing the various possible values for command. This could be simplified in a couple of ways:

  1. Take the logic for each condition and place it in a separate function.
  2. Additionally, replace the now-simplified if/elif conditions with a dictionary whose keys are the valid commands and whose values are the functions specified in 1. Now it becomes a simple matter of checking to see if the entered command is in the dictionary and if so, calling the associated function to process the command.

So Many Global Variables!

Since Python is a language that supports object-oriented programming, consider how your implementation might profit by creating one or more classes whose methods and attributes replace your many functions and global variables. This would make the code more readable, maintainable and extensible.

\$\endgroup\$
3
\$\begingroup\$

Comments

This comment is cryptic:

# gethtmlwithjs

Comments should be used to explain the code, not make it harder to understand. It should be deleted or replaced with something legible.

Delete all commented-out code to reduce clutter:

# elapsed_time = int(time.time() - start)
# print(elapsed_time+1)

Also, delete this docstring containing code:

"""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

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.

Variable names like youshallnotpass should use snake_case (you_shall_not_pass) like many of your other variables.

Simpler

This line:

and youshallnotpass == False

is customarily written as:

and not youshallnotpass

Avoid comparisons to boolean values.

ruff identifies several other places in the code where you do similar comparisons.

ruff also identifies the drewegg variable as unused; you set it, but never use it otherwise.

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.

Partitioning

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

DRY

This string shows up several places in the code:

"\n\n\n\n\n\n\n\n\n\n"

You should set it to a constant, such as:

SEPARATOR = "\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.

\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.