diff --git a/README.md b/README.md index bc66683..1a9e869 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,150 @@ -# wttr.in -Web frontend for wego +## Usage + + +You can access the service from a shell or from a Web browser: + + $ curl wttr.in + Weather for City: Paris, France + + \ / Clear + .-. 10 – 11 °C + ― ( ) ― ↑ 11 km/h + `-’ 10 km + / \ 0.0 mm + + +You can specify the location, for that you want to get the weather information. +If you omit the location name, you will get the information for you current location, +based on your IP address. + + $ curl wttr.in/London + $ curl wttr.in/Moscow + +You can use 3-letters airport codes if you want to get the weather information +about some airports: + + $ curl wttr.in/muc # Weather for IATA: muc, Munich International Airport, Germany + $ curl wttr.in/ham # Weather for IATA: muc, Hamburg Airport, Germany + +You can also use IP-addresses (direct) or domain names (prefixed with @) +as a location specificator: + + $ curl wttr.in/@github.com + $ curl wttr.in/@msu.ru + +To get this information online, you can access the `:help` page: + + $ curl wttr.in/:help + +## Installation + +To install the program you need: + +1. Install external depndencies +2. Install python dependencies used by the service +3. Get WorldWeatherOnline API Key +4. Configure wego +5. Configure wttr.in +6. Configure HTTP-frontend service + +### Install external dependencies + +External requirements: + +* wego [https://github.com/schachmat/wego], weather client for terminal + +To install `wego` you must have golang installed. After that: + + go get https://github.com/schachmat/wego + go install https://github.com/schachmat/wego + +### Install python dependencies + +Python requirements: + +* Flask +* geoip2 +* geopy +* requests +* gevent + +You can install them using `pip`. + +If `virtualenv` is used: + + virtualenv ve + ve/bin/pip install -r requirements.txt + ve/bin/pip bin/srv.py + +Also, you need to install the geoip2 database. +You can use a free database GeoLite2, that can be downloaded from http://dev.maxmind.com/geoip/geoip2/geolite2/ + +### Get WorldWeatherOnline key + +To get the WorldWeatherOnline API key, you must register here: + + https://developer.worldweatheronline.com/auth/register + +### Configure wego + +After you have the key, configure `wego`: + + $ cat ~/.wegorc + { + "APIKey": "00XXXXXXXXXXXXXXXXXXXXXXXXXXX", + "City": "London", + "Numdays": 3, + "Imperial": false, + "Lang": "en" + } + +The `City` parameter in `~/.wegorc` is ignored. + +### Configure wttr.in + +Edit the `bin/srv.py` and specify the path to the local `wttr.in` installation, +to the GeoLite database and to the `wego` installation: + + MYDIR = "/home/igor/wttr.in" + GEOLITE = "/home/igor/wttr.in/GeoLite2-City.mmdb" + WEGO = "/home/igor/go/bin/wego" + + +### Configure HTTP-frontend service + +Configure the web server, that will be used +to access the service (if you want to use a web frontend; it's recommended): + + server { + listen [::]:80; + server_name wttr.in *.wttr.in; + access_log /var/log/nginx/wttr.in-access.log main; + error_log /var/log/nginx/wttr.in-error.log; + + location /clouds_files { root /var/www/igor/; } + location /clouds_images { root /var/www/igor/; } + + location / { + proxy_pass http://127.0.0.1:8002; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + + client_max_body_size 10m; + client_body_buffer_size 128k; + + proxy_connect_timeout 90; + proxy_send_timeout 90; + proxy_read_timeout 90; + + proxy_buffer_size 4k; + proxy_buffers 4 32k; + proxy_busy_buffers_size 64k; + proxy_temp_file_write_size 64k; + + expires off; + } + } + + diff --git a/bin/srv.py b/bin/srv.py new file mode 100644 index 0000000..217bb5e --- /dev/null +++ b/bin/srv.py @@ -0,0 +1,261 @@ +import sys +import logging +import os +import re +import requests +import socket +import subprocess +import time +import traceback + +import geoip2.database +from geopy.geocoders import Nominatim +import jinja2 + +import gevent +from gevent.wsgi import WSGIServer +from gevent.queue import Queue +from gevent.monkey import patch_all +from gevent.subprocess import Popen, PIPE, STDOUT +patch_all() + +from flask import Flask, request, render_template, send_from_directory +app = Flask(__name__) + +MYDIR = os.path.abspath(os.path.dirname( os.path.dirname('__file__') )) +GEOLITE = os.path.join( MYDIR, "GeoLite2-City.mmdb" ) +WEGO = "/home/igor/go/bin/wego" + +CACHEDIR = os.path.join( MYDIR, "cache" ) +IP2LCACHE = os.path.join( MYDIR, "cache/ip2l" ) +ALIASES = os.path.join( MYDIR, "share/aliases" ) +ANSI2HTML = os.path.join( MYDIR, "share/ansi2html.sh" ) +HELP_FILE = os.path.join( MYDIR, 'share/help.txt' ) +LOG_FILE = os.path.join( MYDIR, 'log/main.log' ) +TEMPLATES = os.path.join( MYDIR, 'share/templates' ) +STATIC = os.path.join( MYDIR, 'share/static' ) + +NOT_FOUND_LOCATION = "NOT_FOUND" +DEFAULT_LOCATION = "Oymyakon" + +NOT_FOUND_MESSAGE = """ +We were unable to find your location, +so we have brought you to Oymyakon, +one of the coldest permanently inhabited locales on the planet. +""" + +logging.basicConfig(filename=LOG_FILE, level=logging.DEBUG) + +reader = geoip2.database.Reader(GEOLITE) +geolocator = Nominatim() + +my_loader = jinja2.ChoiceLoader([ + app.jinja_loader, + jinja2.FileSystemLoader(TEMPLATES), +]) +app.jinja_loader = my_loader + + +class Limits: + def __init__( self ): + self.intervals = [ 'min', 'hour', 'day' ] + self.divisor = { + 'min': 60, + 'hour': 3600, + 'day': 86400, + } + self.counter = { + 'min': {}, + 'hour': {}, + 'day': {}, + } + self.limit = { + 'min': 10, + 'hour': 20, + 'day': 100, + } + self.last_update = { + 'min': 0, + 'hour': 0, + 'day': 0, + } + self.clear_counters() + + def check_ip( self, ip ): + self.clear_counters() + for interval in self.intervals: + if ip not in self.counter[interval]: + self.counter[interval][ip] = 0 + self.counter[interval][ip] += 1 + if self.limit[interval] <= self.counter[interval][ip]: + log("Too many queries: %s in %s for %s" % (self.limit[interval], interval, ip) ) + raise RuntimeError("Not so fast! Number of queries per %s is limited to %s" % (interval, self.limit[interval])) + print self.counter + + def clear_counters( self ): + t = int( time.time() ) + for interval in self.intervals: + if t / self.divisor[interval] != self.last_update[interval]: + self.counter[interval] = {} + self.last_update[interval] = t / self.divisor[interval] + + +limits = Limits() + +def error( text ): + print text + raise RuntimeError(text) + +def log( text ): + print text + logging.info( text ) + +def is_ip( ip ): + if re.match('\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', ip) is None: + return False + try: + socket.inet_aton(ip) + return True + except socket.error: + return False + +def save_weather_data( location, filename ): + + if location == NOT_FOUND_LOCATION: + location_not_found = True + location = DEFAULT_LOCATION + else: + location_not_found = False + + p = Popen( [ WEGO, '--city=%s' % location ], stdout=PIPE, stderr=PIPE ) + stdout, stderr = p.communicate() + if p.returncode != 0: + error( stdout + stderr ) + + dirname = os.path.dirname( filename ) + if not os.path.exists( dirname ): + os.makedirs( dirname ) + + if location_not_found: + stdout += NOT_FOUND_MESSAGE + + open( filename, 'w' ).write( stdout ) + + p = Popen( [ "bash", ANSI2HTML, "--palette=solarized", "--bg=dark" ], stdin=PIPE, stdout=PIPE, stderr=PIPE ) + stdout, stderr = p.communicate( stdout ) + if p.returncode != 0: + error( stdout + stderr ) + + open( filename+'.html', 'w' ).write( stdout ) + +def get_filename( location ): + location = location.replace('/', '_') + timestamp = time.strftime( "%Y%m%d%H", time.localtime() ) + return "%s/%s/%s" % ( CACHEDIR, location, timestamp ) + +def get_wetter(location, ip, html=False): + filename = get_filename( location ) + if not os.path.exists( filename ): + limits.check_ip( ip ) + save_weather_data( location, filename ) + if html: + filename += '.html' + return open(filename).read() + + +def ip2location( ip ): + cached = os.path.join( IP2LCACHE, ip ) + + if os.path.exists( cached ): + return open( cached, 'r' ).read() + + try: + t = requests.get( 'http://api.ip2location.com/?ip=%s&key=demo&package=WS10' % ip ).text + if ';' in t: + location = t.split(';')[3] + if not os.path.exists( IP2LCACHE ): + os.makedirs( IP2LCACHE ) + open( cached, 'w' ).write( location ) + return location + except: + pass + +def get_location( ip_addr ): + response = reader.city( ip_addr ) + city = response.city.name + if city is None and response.location: + coord = "%s, %s" % (response.location.latitude, response.location.longitude) + location = geolocator.reverse(coord, language='en') + city = location.raw.get('address', {}).get('city') + if city is None: + print ip_addr + city = ip2location( ip_addr ) + return city or NOT_FOUND_LOCATION + +def load_aliases( aliases_filename ): + aliases_db = {} + with open( aliases_filename, 'r' ) as f: + for line in f.readlines(): + from_, to_ = line.split(':', 1) + aliases_db[ from_.strip().lower() ] = to_.strip() + return aliases_db + +location_alias = load_aliases( ALIASES ) +def location_canonical_name( location ): + if location.lower() in location_alias: + return location_alias[location.lower()] + return location + +def show_help(): + return open(HELP_FILE, 'r').read() + +@app.route('/files/') +def send_static(path): + print path + print STATIC + return send_from_directory(STATIC, path) + +@app.route("/") +@app.route("/") +def wttr(location = None): + user_agent = request.headers.get('User-Agent').lower() + + html_output = True + if 'curl' in user_agent or 'wget' in user_agent: + html_output = False + + if location == ':help': + help_ = show_help() + if html_output: + return render_template( 'index.html', body=help_ ) + else: + return help_ + + orig_location = location + + if request.headers.getlist("X-Forwarded-For"): + ip = request.headers.getlist("X-Forwarded-For")[0] + if ip.startswith('::ffff:'): + ip = ip[7:] + else: + ip = request.remote_addr + + try: + if location is None: + location = get_location( ip ) + + if is_ip( location ): + location = get_location( location ) + if location.startswith('@'): + location = get_location( socket.gethostbyname( location[1:] ) ) + + location = location_canonical_name( location ) + log("%s %s %s %s" % (ip, user_agent, orig_location, location)) + return get_wetter( location, ip, html=html_output ) + except Exception, e: + logging.error("Exception has occured", exc_info=1) + return str(e).rstrip()+"\n" + +server = WSGIServer(("", 8002), app) +server.serve_forever() + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8f21660 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +flask +geoip2 +geopy +requests +gevent diff --git a/share/aliases b/share/aliases new file mode 100644 index 0000000..2459445 --- /dev/null +++ b/share/aliases @@ -0,0 +1,7 @@ +Msk : Moscow +Moskva : Moscow +Moskau : Moscow +Kyiv : Kiev +Kiew : Kiev +Kijev : Kiev +spb : Saint Petersburg diff --git a/share/ansi2html.sh b/share/ansi2html.sh new file mode 100644 index 0000000..b4561d6 --- /dev/null +++ b/share/ansi2html.sh @@ -0,0 +1,498 @@ +#!/bin/sh + +# Convert ANSI (terminal) colours and attributes to HTML + +# Licence: LGPLv2 +# Author: +# http://www.pixelbeat.org/docs/terminal_colours/ +# Examples: +# ls -l --color=always | ansi2html.sh > ls.html +# git show --color | ansi2html.sh > last_change.html +# Generally one can use the `script` util to capture full terminal output. +# Changes: +# V0.1, 24 Apr 2008, Initial release +# V0.2, 01 Jan 2009, Phil Harnish +# Support `git diff --color` output by +# matching ANSI codes that specify only +# bold or background colour. +# P@draigBrady.com +# Support `ls --color` output by stripping +# redundant leading 0s from ANSI codes. +# Support `grep --color=always` by stripping +# unhandled ANSI codes (specifically ^[[K). +# V0.3, 20 Mar 2009, http://eexpress.blog.ubuntu.org.cn/ +# Remove cat -v usage which mangled non ascii input. +# Cleanup regular expressions used. +# Support other attributes like reverse, ... +# P@draigBrady.com +# Correctly nest tags (even across lines). +# Add a command line option to use a dark background. +# Strip more terminal control codes. +# V0.4, 17 Sep 2009, P@draigBrady.com +# Handle codes with combined attributes and color. +# Handle isolated attributes with css. +# Strip more terminal control codes. +# V0.23, 22 Dec 2015 +# http://github.com/pixelb/scripts/commits/master/scripts/ansi2html.sh + +gawk --version >/dev/null || exit 1 + +if [ "$1" = "--version" ]; then + printf '0.22\n' && exit +fi + +if [ "$1" = "--help" ]; then + printf '%s\n' \ +'This utility converts ANSI codes in data passed to stdin +It has 2 optional parameters: + --bg=dark --palette=linux|solarized|tango|xterm +E.g.: ls -l --color=always | ansi2html.sh --bg=dark > ls.html' >&2 + exit +fi + +[ "$1" = "--bg=dark" ] && { dark_bg=yes; shift; } + +if [ "$1" = "--palette=solarized" ]; then + # See http://ethanschoonover.com/solarized + P0=073642; P1=D30102; P2=859900; P3=B58900; + P4=268BD2; P5=D33682; P6=2AA198; P7=EEE8D5; + P8=002B36; P9=CB4B16; P10=586E75; P11=657B83; + P12=839496; P13=6C71C4; P14=93A1A1; P15=FDF6E3; + shift; +elif [ "$1" = "--palette=solarized-xterm" ]; then + # Above mapped onto the xterm 256 color palette + P0=262626; P1=AF0000; P2=5F8700; P3=AF8700; + P4=0087FF; P5=AF005F; P6=00AFAF; P7=E4E4E4; + P8=1C1C1C; P9=D75F00; P10=585858; P11=626262; + P12=808080; P13=5F5FAF; P14=8A8A8A; P15=FFFFD7; + shift; +elif [ "$1" = "--palette=tango" ]; then + # Gnome default + P0=000000; P1=CC0000; P2=4E9A06; P3=C4A000; + P4=3465A4; P5=75507B; P6=06989A; P7=D3D7CF; + P8=555753; P9=EF2929; P10=8AE234; P11=FCE94F; + P12=729FCF; P13=AD7FA8; P14=34E2E2; P15=EEEEEC; + shift; +elif [ "$1" = "--palette=xterm" ]; then + P0=000000; P1=CD0000; P2=00CD00; P3=CDCD00; + P4=0000EE; P5=CD00CD; P6=00CDCD; P7=E5E5E5; + P8=7F7F7F; P9=FF0000; P10=00FF00; P11=FFFF00; + P12=5C5CFF; P13=FF00FF; P14=00FFFF; P15=FFFFFF; + shift; +else # linux console + P0=000000; P1=AA0000; P2=00AA00; P3=AA5500; + P4=0000AA; P5=AA00AA; P6=00AAAA; P7=AAAAAA; + P8=555555; P9=FF5555; P10=55FF55; P11=FFFF55; + P12=5555FF; P13=FF55FF; P14=55FFFF; P15=FFFFFF; + [ "$1" = "--palette=linux" ] && shift +fi + +[ "$1" = "--bg=dark" ] && { dark_bg=yes; shift; } + +# Mac OSX's GNU sed is installed as gsed +# use e.g. homebrew 'gnu-sed' to get it +if ! sed --version >/dev/null 2>&1; then + if gsed --version >/dev/null 2>&1; then + alias sed=gsed + else + echo "Error, can't find an acceptable GNU sed." >&2 + exit 1 + fi +fi + +printf '%s' " + + + + + + + +
+'
+
+p='\x1b\['        #shortcut to match escape codes
+
+# Handle various xterm control sequences.
+# See /usr/share/doc/xterm-*/ctlseqs.txt
+sed "
+# escape ampersand and quote
+s#&#\&#g; s#\"#\"#g;
+s#\x1b[^\x1b]*\x1b\\\##g  # strip anything between \e and ST
+s#\x1b][0-9]*;[^\a]*\a##g # strip any OSC (xterm title etc.)
+
+s#\r\$## # strip trailing \r
+
+# strip other non SGR escape sequences
+s#[\x07]##g
+s#\x1b[]>=\][0-9;]*##g
+s#\x1bP+.\{5\}##g
+# Mark cursor positioning codes \"Jr;c;
+s#${p}\([0-9]\{1,2\}\)G#\"J;\1;#g
+s#${p}\([0-9]\{1,2\}\);\([0-9]\{1,2\}\)H#\"J\1;\2;#g
+
+# Mark clear as \"Cn where n=1 is screen and n=0 is to end-of-line
+s#${p}H#\"C1;#g
+s#${p}K#\"C0;#g
+# Mark Cursor move columns as \"Mn where n is +ve for right, -ve for left
+s#${p}C#\"M1;#g
+s#${p}\([0-9]\{1,\}\)C#\"M\1;#g
+s#${p}\([0-9]\{1,\}\)D#\"M-\1;#g
+s#${p}\([0-9]\{1,\}\)P#\"X\1;#g
+
+s#${p}[0-9;?]*[^0-9;?m]##g
+
+" |
+
+# Normalize the input before transformation
+sed "
+# escape HTML (ampersand and quote done above)
+s#>#\>#g; s#<#\<#g;
+
+# normalize SGR codes a little
+
+# split 256 colors out and mark so that they're not
+# recognised by the following 'split combined' line
+:e
+s#${p}\([0-9;]\{1,\}\);\([34]8;5;[0-9]\{1,3\}\)m#${p}\1m${p}¬\2m#g; t e
+s#${p}\([34]8;5;[0-9]\{1,3\}\)m#${p}¬\1m#g;
+
+:c
+s#${p}\([0-9]\{1,\}\);\([0-9;]\{1,\}\)m#${p}\1m${p}\2m#g; t c   # split combined
+s#${p}0\([0-7]\)#${p}\1#g                                 #strip leading 0
+s#${p}1m\(\(${p}[4579]m\)*\)#\1${p}1m#g                   #bold last (with clr)
+s#${p}m#${p}0m#g                                          #add leading 0 to norm
+
+# undo any 256 color marking
+s#${p}¬\([34]8;5;[0-9]\{1,3\}\)m#${p}\1m#g;
+
+# map 16 color codes to color + bold
+s#${p}9\([0-7]\)m#${p}3\1m${p}1m#g;
+s#${p}10\([0-7]\)m#${p}4\1m${p}1m#g;
+
+# change 'reset' code to \"R
+s#${p}0m#\"R;#g
+" |
+
+# Convert SGR sequences to HTML
+sed "
+# common combinations to minimise html (optional)
+:f
+s#${p}3[0-7]m${p}3\([0-7]\)m#${p}3\1m#g; t f
+:b
+s#${p}4[0-7]m${p}4\([0-7]\)m#${p}4\1m#g; t b
+s#${p}3\([0-7]\)m${p}4\([0-7]\)m##g
+s#${p}4\([0-7]\)m${p}3\([0-7]\)m##g
+
+s#${p}1m##g
+s#${p}4m##g
+s#${p}5m##g
+s#${p}7m##g
+s#${p}9m##g
+s#${p}3\([0-9]\)m##g
+s#${p}4\([0-9]\)m##g
+
+s#${p}38;5;\([0-9]\{1,3\}\)m##g
+s#${p}48;5;\([0-9]\{1,3\}\)m##g
+
+s#${p}[0-9;]*m##g # strip unhandled codes
+" |
+
+# Convert alternative character set and handle cursor movement codes
+# Note we convert here, as if we do at start we have to worry about avoiding
+# conversion of SGR codes etc., whereas doing here we only have to
+# avoid conversions of stuff between &...; or <...>
+#
+# Note we could use sed to do this based around:
+#   sed 'y/abcdefghijklmnopqrstuvwxyz{}`~/▒␉␌␍␊°±␤␋┘┐┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥π£◆·/'
+# However that would be very awkward as we need to only conv some input.
+# The basic scheme that we do in the awk script below is:
+#  1. enable transliterate once "T1; is seen
+#  2. disable once "T0; is seen (may be on diff line)
+#  3. never transliterate between &; or <> chars
+#  4. track x,y movements and active display mode at each position
+#  5. buffer line/screen and dump when required
+sed "
+# change 'smacs' and 'rmacs' to \"T1 and \"T0 to simplify matching.
+s#\x1b(0#\"T1;#g;
+s#\x0E#\"T1;#g;
+
+s#\x1b(B#\"T0;#g
+s#\x0F#\"T0;#g
+" |
+(
+gawk '
+function dump_line(l,del,c,blanks,ret) {
+  for(c=1;c")
+  for(i=1;i<=spc;i++) {
+    rm=rm?rm:(a[i]!=attr[i]">")
+    if(rm) {
+      ret=ret ""
+      delete a[i];
+    }
+  }
+  for(i=1;i"
+    if(a[i]!=attr[i]) {
+      a[i]=attr[i]
+      ret = ret attr[i]
+    }
+  }
+  return ret
+}
+
+function encode(string,start,end,i,ret,pos,sc,buf) {
+   if(!end) end=length(string);
+   if(!start) start=1;
+   state=3
+   for(i=1;i<=length(string);i++) {
+     c=substr(string,i,1)
+     if(state==2) {
+       sc=sc c
+       if(c==";") {
+          c=sc
+          state=last_mode
+       } else continue
+     } else {
+       if(c=="\r") { x=1; continue }
+       if(c=="<") {
+         # Change attributes - store current active
+         # attributes in span array
+         split(substr(string,i),cord,">");
+         i+=length(cord[1])
+         span[++spc]=cord[1] ">"
+         continue
+       }
+       else if(c=="&") {
+         # All goes to single position till we see a semicolon
+         sc=c
+         state=2
+         continue
+       }
+       else if(c=="\b") {
+          # backspace move insertion point back 1
+          if(spc) attr[x,y]=atos(span)
+          x=x>1?x-1:1
+          continue
+       }
+       else if(c=="\"") {
+          split(substr(string,i+2),cord,";")
+          cc=substr(string,i+1,1);
+          if(cc=="T") {
+              # Transliterate on/off
+              if(cord[1]==1&&state==3) last_mode=state=4
+              if(cord[1]==0&&state==4) last_mode=state=3
+          }
+          else if(cc=="C") {
+              # Clear
+              if(cord[1]+0) {
+                # Screen - if Recording dump screen
+                if(dumpStatus==dsActive) ret=ret dump_screen()
+                dumpStatus=dsActive
+                delete dump
+                delete attr
+                x=y=1
+              } else {
+                # To end of line
+                for(pos=x;posmaxY) maxY=y
+                # Change y - start recording
+                dumpStatus=dumpStatus?dumpStatus:dsReset
+              }
+          }
+          else if(cc=="M") {
+              # Move left/right on current line
+              x+=cord[1]
+          }
+          else if(cc=="X") {
+              # delete on right
+              for(pos=x;pos<=maxX;pos++) {
+                nx=pos+cord[1]
+                if(nx=start&&i<=end&&c in Trans) c=Trans[c]
+     }
+     if(dumpStatus==dsReset) {
+       delete dump
+       delete attr
+       ret=ret"\n"
+       dumpStatus=dsActive
+     }
+     if(dumpStatus==dsNew) {
+       # After moving/clearing we are now ready to write
+       # somthing to the screen so start recording now
+       ret=ret"\n"
+       dumpStatus=dsActive
+     }
+     if(dumpStatus==dsActive||dumpStatus==dsOff) {
+       dump[x,y] = c
+       if(!spc) delete attr[x,y]
+       else attr[x,y] = atos(span)
+       if(++x>maxX) maxX=x;
+     }
+    }
+    # End of line if dumping increment y and set x back to first col
+    x=1
+    if(!dumpStatus) return ret dump_line(y,1);
+    else if(++y>maxY) maxY=y;
+    return ret
+}
+BEGIN{
+  OFS=FS
+  # dump screen status
+  dsOff=0    # Not dumping screen contents just write output direct
+  dsNew=1    # Just after move/clear waiting for activity to start recording
+  dsReset=2  # Screen cleared build new empty buffer and record
+  dsActive=3 # Currently recording
+  F="abcdefghijklmnopqrstuvwxyz{}`~"
+  T="▒␉␌␍␊°±␤␋┘┐┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥π£◆·"
+  maxX=80
+  delete cur;
+  x=y=1
+  for(i=1;i<=length(F);i++)Trans[substr(F,i,1)]=substr(T,i,1);
+}
+
+{ $0=encode($0) }
+1
+END {
+  if(dumpStatus) {
+    print dump_screen();
+  }
+}'
+)
+
+printf '
+ +\n' diff --git a/share/help.txt b/share/help.txt new file mode 100644 index 0000000..41bc630 --- /dev/null +++ b/share/help.txt @@ -0,0 +1,15 @@ +Usage: + + $ curl wttr.in # current location + $ curl wttr.in/muc # weather in the Munic airport + +Supported locations: + + /paris # city name + /muc # airport code (3 letters) + /@stackoverflow.com # domain name + +Special URLs: + + /:help # show this page + diff --git a/share/static/style.css b/share/static/style.css new file mode 100644 index 0000000..ba8827a --- /dev/null +++ b/share/static/style.css @@ -0,0 +1,4 @@ +body { + background: black; + color: #bbbbbb; +} diff --git a/share/templates/index.html b/share/templates/index.html new file mode 100644 index 0000000..cf92bb9 --- /dev/null +++ b/share/templates/index.html @@ -0,0 +1,10 @@ + + + + + +
+{{ body }}
+
+ +