From 328f2998b27ef189dac893d081d3626854fc20d4 Mon Sep 17 00:00:00 2001 From: mounderfod Date: Wed, 19 Jul 2023 14:40:29 +0200 Subject: Initial commit --- .gitignore | 2 + .idea/.gitignore | 8 ++ .idea/encodings.xml | 6 ++ .idea/gophersite.iml | 21 +++++ .idea/inspectionProfiles/Project_Default.xml | 12 +++ .idea/inspectionProfiles/profiles_settings.xml | 6 ++ .idea/misc.xml | 4 + .idea/modules.xml | 8 ++ __pycache__/app.cpython-311.pyc | Bin 0 -> 952 bytes __pycache__/news.cpython-311.pyc | Bin 0 -> 2642 bytes __pycache__/weather.cpython-311.pyc | Bin 0 -> 7354 bytes app.py | 47 +++++++++++ news.py | 34 ++++++++ personal/gophermap | 13 +++ personal/phlog/23-06-23-welcome.txt | 15 ++++ personal/phlog/23-07-11-fediverse.txt | 94 +++++++++++++++++++++ personal/phlog/23-07-18-emacs.txt | 60 +++++++++++++ personal/phlog/gophermap | 7 ++ requirements.txt | 4 + static/ascii/cat.txt | 20 +++++ weather.py | 112 +++++++++++++++++++++++++ 21 files changed, 473 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/encodings.xml create mode 100644 .idea/gophersite.iml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 __pycache__/app.cpython-311.pyc create mode 100644 __pycache__/news.cpython-311.pyc create mode 100644 __pycache__/weather.cpython-311.pyc create mode 100644 app.py create mode 100644 news.py create mode 100644 personal/gophermap create mode 100644 personal/phlog/23-06-23-welcome.txt create mode 100644 personal/phlog/23-07-11-fediverse.txt create mode 100644 personal/phlog/23-07-18-emacs.txt create mode 100644 personal/phlog/gophermap create mode 100644 requirements.txt create mode 100644 static/ascii/cat.txt create mode 100644 weather.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9bec4dc --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +venv/ \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..c3a317a --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gophersite.iml b/.idea/gophersite.iml new file mode 100644 index 0000000..e2b9085 --- /dev/null +++ b/.idea/gophersite.iml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..d4a00d6 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..4cfc957 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..1b01f16 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/__pycache__/app.cpython-311.pyc b/__pycache__/app.cpython-311.pyc new file mode 100644 index 0000000..dd2a1a1 Binary files /dev/null and b/__pycache__/app.cpython-311.pyc differ diff --git a/__pycache__/news.cpython-311.pyc b/__pycache__/news.cpython-311.pyc new file mode 100644 index 0000000..da743bd Binary files /dev/null and b/__pycache__/news.cpython-311.pyc differ diff --git a/__pycache__/weather.cpython-311.pyc b/__pycache__/weather.cpython-311.pyc new file mode 100644 index 0000000..7fa807e Binary files /dev/null and b/__pycache__/weather.cpython-311.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..702045c --- /dev/null +++ b/app.py @@ -0,0 +1,47 @@ +import os +from os import listdir +from os.path import isfile, join +from dotenv import load_dotenv +from pituophis import Item, serve +from pyfiglet import Figlet +import news +import weather + +load_dotenv() +figlet = Figlet(font="big") + + +def handle(request): + if request.path == "" or request.path == "/": + menu = [] + with open("static/ascii/cat.txt", "r") as f: + menu += [Item(text=i) for i in f.readlines()] + + menu.append(Item(itype="1", text="......NEWS", path="/news", host=os.getenv("HOSTNAME"))) + menu.append( + Item(itype="7", text="......WEATHER (type in city name)", path="/weather", host=os.getenv("HOSTNAME"))) + menu.append(Item(itype="1", text="......OWNER'S SITE", path="/personal", host=os.getenv("HOSTNAME"))) + return menu + elif request.path.startswith("/newstxt"): + return news.get_newstxt(request.path.split("?article=")[1]) + elif request.path.startswith("/weathertxt"): + return weather.get_weather(request.path) + elif request.path == "/personal": + with open("personal/gophermap", "r") as f: + return [i for i in f.readlines()] + elif request.path == "/news" or request.path == "/weather": + menu = [] + text = figlet.renderText(request.path[1:]).split("\n") + menu += [Item(text=i) for i in text] + match request.path: + case "/news": + menu.append(Item(text="=== Provided by The Guardian ===")) + menu += news.get_news() + case "/weather": + menu += weather.get_cities(request.query) + return menu + else: + return [Item(itype="3", text="Page not found")] + + +serve(os.getenv("HOSTNAME"), port=70, handler=handle) diff --git a/news.py b/news.py new file mode 100644 index 0000000..6894d6d --- /dev/null +++ b/news.py @@ -0,0 +1,34 @@ +import os +import urllib.parse +import html2text +import requests +from dotenv import load_dotenv +from pituophis import Item + +load_dotenv() + +def get_news(): + results = [] + data = requests.get(f"https://content.guardianapis.com/search?api-key={os.getenv('GUARDIAN_API')}&page-size=50").json() + results += [Item( + itype="0", + text=i['webTitle'], + path=f"/newstxt?article={urllib.parse.quote(i['id'], safe='')}", + host=os.getenv("HOSTNAME") + ) for i in data['response']['results']] + return results + + +def get_newstxt(article): + path = urllib.parse.unquote(article) + data = requests.get(f"https://content.guardianapis.com/{path}?api-key={os.getenv('GUARDIAN_API')}&show-fields=body").json() + h = html2text.HTML2Text() + h.use_automatic_links = True + h.images_to_alt = True + h.ignore_tables = True + h.ignore_links = True + h.emphasis_mark = "" + h.strong_mark = "" + text = f"{data['response']['content']['webTitle']}\n" + ("=" * 60) + "\n" + text += h.handle(data['response']['content']['fields']['body']) + return text \ No newline at end of file diff --git a/personal/gophermap b/personal/gophermap new file mode 100644 index 0000000..71ffe3e --- /dev/null +++ b/personal/gophermap @@ -0,0 +1,13 @@ +i \ /\ ---------------- null.host 1 +i ) ( ') <| mounderfod | null.host 1 +i ( / ) ---------------- null.host 1 +i \(__)| null.host 1 +i null.host 1 +iWelcome to my gopher space! My name is Noah, but I go by mounderfod. null.host 1 +iI am interested in music, programming and video games. null.host 1 +i null.host 1 +iLanguages I speak: French, English null.host 1 +iPronouns: he/him null.host 1 +i null.host 1 +hMy WWW site URL:https://mounderfod.online gopher.mounderfod.online 70 +1Phlog (mirror of my blog) /personal/phlog gopher.mounderfod.online 70 diff --git a/personal/phlog/23-06-23-welcome.txt b/personal/phlog/23-06-23-welcome.txt new file mode 100644 index 0000000..dd74824 --- /dev/null +++ b/personal/phlog/23-06-23-welcome.txt @@ -0,0 +1,15 @@ +Welcome to my blog! +=================== +23 June 2023 | https://www.mounderfod.online/2023/06/23/welcome-to-my-blog.html + +Hello, I have decided to set up a blog on my website :) +Basically, I will use this page to create more long-form posts to express various ideas and other things that +I've found cool recently. + +How does the website work? +-------------------------- +The frontend was all written by me in raw HTML/CSS. +The little time marquee at the top of the page was my own idea, +but I "borrowed" the time formatting code from Stack Overflow. +In terms of backend, the website uses Jekyll and is being hosted on GitHub using Vercel to deploy it. +If anything is not working or you just want to contact me, contact me on Discord @mounderfod. diff --git a/personal/phlog/23-07-11-fediverse.txt b/personal/phlog/23-07-11-fediverse.txt new file mode 100644 index 0000000..ef30e1b --- /dev/null +++ b/personal/phlog/23-07-11-fediverse.txt @@ -0,0 +1,94 @@ +Enter The Fediverse +=================== +11 July 2023 | https://www.mounderfod.online/2023/07/11/enter-the-fediverse.html + +If you havent heard, Reddit is in a bit of a pickle. In short, they have changed their API pricing +in such a way as to effectively make it impossible for 3rd party apps to continue, presumably in +order to improve that sweet ad revenue (a move probably inspired by Twitter's). In any case, the lack +of Reddit for a few days (and my general dissatisfaction with the platform at that time) led me to +explore alternatives, which led me to Lemmy, a finding which would cause me to dive much deeper into a much +wider thing - the Fediverse. This article will explore this process and how Ive found it so far. + +Lemmy +----- +As I said, the first thing that I'd found for this was Lemmy. Lemmy is, according to its own website: + +"a selfhosted social link aggregation and discussion platform. It is completely free and open, and not +controlled by any company. This means that there is no advertising, tracking, or secret algorithms. Content is +organized into communities, so it is easy to subscribe to topics that you are interested in, and ignore others. +Voting is used to bring the most interesting items to the top." + +Sounds cool, and most importantly, very similar to Reddit UX-wise, so I went and made an account. + +I should probably explain something out of the gate - there isnt one Lemmy website. Thats the whole point of +the Fediverse. Instead, there are many instances of Lemmy, all of which are federated, that is to say, they are +all interconnected. From my instance, I can make a post on another instance, which a user from yet another +instance can comment on. This way, there is no central authority for the whole site. Its like crypto, but +without the enormous range of scams and profiteering. + +Anyway, the instance that I decided to start with was lemmy.blahaj.zone. I was told that the instance you start +with doesnt really matter, so I chose this one on somewhat silly grounds - I own a Blahaj myself. + +It worked pretty well to begin with, as from my perspective it seemed to be working just like Reddit; I could +make posts, join communities, and so on. However, I quickly noticed that choosing an instance wasnt going to be +as simple for me as I had anticipated, due to my own pickiness and the particulars of federation. + +How federation works (a crude explanation) +------------------------------------------ +Obviously, it would be wildly inefficient if every server shared everything that happened on it with every +server. As a result, federation on ActivityPub (the protocol that Lemmy, Mastodon, and more - including the +dreaded Meta Threads - use as their base) works like this: + +1. a user on server A requests a post/user/community on server B by using the search function +2. server B provides what they requested and creates a federated link between it and server A +3. any future activity on the post/user/community is sent to server A for anyone to see + +This is probably an oversimplification, and it doesnt work 100% of the time (particularly when federating +between servers running different software), but from my observations this is basically how it works. + +This is also where my issue with the instance Id chosen came up. There was nothing inherently wrong with the +instance or its admins, and in fact it was a lovely instance, but several of the communities that I was +interested in (such as the Modded Minecraft or RetroWeb ones) had not yet federated properly. This is no fault +of the instances, and once I had searched for them myself, any new activity would be visible to me. But I am an +impatient moron, and I wanted to see what the existing activity was, and besides I began to notice that not all +comments on posts were being federated to me (such that going to the home instance of the post showed more +comments than I could see or respond to on my own instance), and this bothered me for some reason, so I decided +that I would begin to look for another instance. + +Side note: This issue is probably fixed now as the instance has grown some since the time of this experience. + +Conveniently, it became apparent to me on that same day that the SDF network, a lovely network of Internet +services, including a public Unix shell, had recently set up a Lemmy server. I had been a member of SDF for a +couple of weeks, and had been using their IRC and bulletin board during that time, so I figured I would set up +an account there - and besides, I checked in advance, and the federation seemed to be more to my liking - so +now Ive been using lemmy.sdf.org as my home instance and its been great - my username is +@mounderfod@lemmy.sdf.org if youre curious. + +Mastodon +-------- +Of course, if youre familiar with the Fediverse (or have read the article up to this point), +you know that Lemmy is not the only Fediverse service that exists. Next for me was a replacement for Twitter, +which Id ditched on the day it was bought by Elon Musk (a fact which turned out to be excellent foresight on my +part, but realistically I was going to delete my account anyway, so it wasnt exactly a stroke of genius). + +This one was a little easier, since I discovered that SDF also had a Mastodon instance. I have to say that in +recent times Ive found myself using Mastodon a lot more than Lemmy; theres more users so theres more content +for me to access, and the lack of algorithm is really refreshing because it allows me to build my feed with +only the content (and people) that I want to see. + +If youd like to follow me on Mastodon my account is @mounderfod@mastodon.sdf.org. + +Other services and conclusion +----------------------------- +By this point I was fully immersed in the Fediverse, and quickly set up Pixelfed to replace Instagram and +Funkwhale to store my personal music collection. I also intend to set up my website with IndieWeb, which is +basically federation for blogs. + +At the start of this article, I explained that this all started because of Reddits API changes. But the truth +is, I was starting to become disillusioned with the mainstream social media networks long before this, with the +constant algorithms, ragebait and promotion of far-right content putting a drain on my own energy. I was +perhaps longing for freedom to control what I put my attention on. If this sounds like you, then I would +strongly recommend that you at least try the Fediverse networks - in any case, its much easier to delete your +account with them than with e.g. Facebook if you dont like it! + +I hope this article was of at least some interest to you, and thank you for making it this far :D diff --git a/personal/phlog/23-07-18-emacs.txt b/personal/phlog/23-07-18-emacs.txt new file mode 100644 index 0000000..f59db3f --- /dev/null +++ b/personal/phlog/23-07-18-emacs.txt @@ -0,0 +1,60 @@ +Using Emacs +=========== +18 July 2023 | https://www.mounderfod.online/2023/07/18/using-emacs.html + +This post is being written in Emacs :) + +What? +----- +Emacs is, according to its own website: + +"An extensible, customizable, free/libre text editor and more." + +Basically, its one of the oldest text editors to exist, is (technically) entirely keyboard-based, and manages to combine simplicity with +power. In short, its great and Im going to talk about it now. + +Why? +---- +Why am I using Emacs? Well, theres a few reasons: + +- Id heard of it before and it sounded cool +- Its complex enough that it would present an interesting learning curve, but not so difficult as to discourage me +- Its useful for editing posts and HTML like this +- It ships with Tetris built in (need I say more?) + +How? +---- +I went to the website and downloaded it. My laptop currently uses Windows, and Emacs is made by GNU so as +expected I was berated for my choice of OS: + +"To improve the use of proprietary systems is a misguided goal. Our aim, rather, is to eliminate them." + +But I wasnt going to concern myself with GNUs plans for world domination; thats a problem for another day. The install was fairly simple, like any other +application, and upon running the program I am greeted with a pleasant menu screen. + +Now it was time for me to learn how to use Emacs. Emacs is primarily keyboard-based, as it was developed at a time where not all computers had GUIs at all, +let alone mice to interact with them. As such, and also due to its age, it has its own set of keybinding patterns which are overall very different to that +of most applications. For example, saving a file in MS Word is Ctrl-S, while in Emacs it is C-x C-s, which means Ctrl-x followed by Ctrl-S. Youll notice that in +this example, two keybindings need to be pressed to perform one action. This is common in Emacs, as there are lots of commands and not many keys, +and there are even some commands that dont have keybindings and must be invoked by pressing M-x (M meaning Alt) and then typing the command name out. + +This was all a bit complex for me to understand at first, but I quickly got the hang of it (as I had done with the more standard keybinding patterns that existed +elsewhere in the computing world). + +Customising Emacs +----------------- +Now that I had gotten the grips of Emacs' basic usage, I needed to tailor it to my own needs. My plan was to use Emacs for editing Markdown posts (such as this one) +or HTML files, and my website is hosted on GitHub, so I needed something to cover both bases. + +For the latter, there was already Emacs VersionControl, but this was a generic version control tool +and wasnt tailored to the specifics of Git. Therefore, I did some googling and came across Magit. A few more googles educated me in how to add the package +repository it was in and how to then install the package (M-x package-install RET magit RET), and I was quickly able to clone, commit, and push to the website +repository. Perfect! Now I needed to improve my Markdown editing experience. .md files are text, and so I could edit them as normal in Emacs, but then I wouldnt +be able to enjoy things such as syntax highlighting and easy access to various formatting options without typing them out manually. Again, a quick google found +markdown-mode, and within moments it was installed. The package adds a major mode to Emacs - Emacs is mode-based, meaning that there are modes of editing which +result in different functionality of the editor for different purposes - in this case, the markdown mode (enabled with M-x markdown-mode) provides syntax +highlighting and commands to automatically paste in the syntax for links, etc. + +And that was it! +I had installed, learned to use, and configured Emacs and could now use it to edit blog posts for this very website (or Gopher phlog, if youre reading it on that +mirror). Next I shall get it set up for developing my Python/Java projects - Ill keep you posted! diff --git a/personal/phlog/gophermap b/personal/phlog/gophermap new file mode 100644 index 0000000..71ea7c9 --- /dev/null +++ b/personal/phlog/gophermap @@ -0,0 +1,7 @@ +1Back to homepage / gopher.mounderfod.online 70 +i null.host 1 +imounderfod's Phlog (Gopher blog) null.host 1 +i null.host 1 +018 July 2023 - Using Emacs /personal/phlog/23-07-18-emacs.txt gopher.mounderfod.online 70 +011 July 2023 - Enter the Fediverse /personal/phlog/23-07-11-fediverse.txt gopher.mounderfod.online 70 +023 June 2023 - Welcome to my blog! /personal/phlog/23-06-23-welcome.txt gopher.mounderfod.online 70 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3f7a82b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +python-dotenv~=1.0.0 +Pituophis~=1.1 +pyfiglet~=0.7.6 +requests~=2.31.0 \ No newline at end of file diff --git a/static/ascii/cat.txt b/static/ascii/cat.txt new file mode 100644 index 0000000..41fb038 --- /dev/null +++ b/static/ascii/cat.txt @@ -0,0 +1,20 @@ + _ __ _ + | | / _| | | + _ __ ___ ___ _ _ _ __ __| | ___ _ __| |_ ___ __| | +| '_ ` _ \ / _ \| | | | '_ \ / _` |/ _ \ '__| _/ _ \ / _` | +| | | | | | (_) | |_| | | | | (_| | __/ | | || (_) | (_| | +|_| |_| |_|\___/ \__,_|_| |_|\__,_|\___|_| |_| \___/ \__,_| + + ================================== + \ /\ | Welcome to the MOUNDERFOD gopher | + ) ( ') < server, ran as part of the OSLI | + ( / ) | services by "mounderfod" | + \(__)| ================================== + +(ascii art by Joan Stark) + ++------------------------------------------+ +| | +| DIRECTORIES | +| | ++------------------------------------------+ diff --git a/weather.py b/weather.py new file mode 100644 index 0000000..50b947a --- /dev/null +++ b/weather.py @@ -0,0 +1,112 @@ +import urllib.parse +import os +import requests +from pituophis import Item +from dotenv import load_dotenv +from prettytable import PrettyTable +from pyfiglet import Figlet + +load_dotenv() + + +def get_code(code): + match code: + case 0: + return "Clear sky" + case 1 | 2 | 3: + return "Partly cloudy" + case 45 | 48: + return "Foggy" + case 51 | 53 | 55: + return "Drizzle" + case 56 | 57: + return "Freezing drizzle" + case 61 | 63 | 65: + return "Rain" + case 66 | 67: + return "Freezing rain" + case 71 | 73 | 675: + return "Snow" + case 77: + return "Snow grains" + case 80 | 81 | 82: + return "Rain showers" + case 85 | 86: + return "Snow showers" + case 95: + return "Thunderstorm" + case 96 | 99: + return "Thunderstorm with hail" + case _: + return "Unknown" + + +def get_cities(query): + data = requests.get( + f"https://geocoding-api.open-meteo.com/v1/search?name={query}&count=10&language=en&format=json").json() + if "results" in data: + return [Item( + itype="0", + text=f"{i['name']} ({i['admin1'] + ', ' if 'admin1' in i else ''}{i['country']})", + path=f"/weathertxt?latlong={str(i['latitude'])}@{str(i['longitude'])}&city={i['name']}", + host=os.getenv("HOSTNAME") + ) for i in data['results']] + else: + return [Item(itype="3", text="City could not be found")] + + +def get_weather(city): + result = [] + query = city.split("?latlong=")[1] + latitude = query.split("&city=")[0].split("@")[0] + longitude = query.split("&city=")[0].split("@")[1] + place = query.split("&city=")[1] + + print(f"https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}&hourly" + f"=temperature_2m,relativehumidity_2m,precipitation_probability," + f"weathercode,windspeed_10m&daily=weathercode," + f"temperature_2m_max,temperature_2m_min,sunrise," + f"sunset¤t_weather=true&timezone=auto") + + data = requests.get(f"https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}&hourly" + f"=temperature_2m,relativehumidity_2m,precipitation_probability," + f"weathercode,windspeed_10m&daily=weathercode," + f"temperature_2m_max,temperature_2m_min,sunrise," + f"sunset¤t_weather=true&timezone=auto").json() + + f = Figlet(font="big") + result += f.renderText("weather").split("\n") + f.setFont(font="small") + + result.append("="*80) + result.append(f"{place}".center(80)) + result.append("="*80) + + result += f.renderText("hourly").split("\n") + table_hourly = PrettyTable() + table_hourly.field_names = ["Time", "Summary", "Temperature", "Humidity", "Precipitation Chance", "Wind Speed"] + for idx, i in enumerate(data['hourly']['time'][:23]): + table_hourly.add_row([ + i[-5:], + get_code(data['hourly']['weathercode'][idx]), + f"{data['hourly']['temperature_2m'][idx]} C", + f"{data['hourly']['relativehumidity_2m'][idx]} %", + f"{data['hourly']['precipitation_probability'][idx]} %", + f"{data['hourly']['windspeed_10m'][idx]} km/h", + ]) + result.append(table_hourly.get_string()) + result += ["\n"] * 3 + + result += f.renderText("daily").split("\n") + table_daily = PrettyTable() + table_daily.field_names = ["Date", "Summary", "Temperature (Max/Min)", "Sunrise / Sunset"] + for idx, i in enumerate(data['daily']['time']): + table_daily.add_row([ + i, + get_code(data['daily']['weathercode'][idx]), + f"{data['daily']['temperature_2m_max'][idx]} C / {data['daily']['temperature_2m_min'][idx]} C", + f"{data['daily']['sunrise'][idx][-5:]} / {data['daily']['sunset'][idx][-5:]}" + ]) + result.append(table_daily.get_string()) + + return result -- cgit 1.4.1-2-gfad0