├── __init__.py ├── tests ├── __init__.py ├── test_utils.py ├── test_shortener.py ├── test_twitter_api.py └── test_complete.py ├── src └── tyrs │ ├── __init__.py │ ├── shorter │ ├── __init__.py │ ├── custom.py │ ├── urlshorter.py │ ├── ur1ca.py │ ├── bitly.py │ ├── msudpl.py │ └── googl.py │ ├── container.py │ ├── completion.py │ ├── update.py │ ├── filter.py │ ├── user.py │ ├── utils.py │ ├── tyrs.py │ ├── editor.py │ ├── message.py │ ├── constant.py │ ├── timeline.py │ ├── help.py │ ├── keys.py │ ├── interface.py │ ├── widget.py │ ├── config.py │ └── tweets.py ├── doc ├── reference-dev.txt ├── tyrs.1.gz ├── build_site.sh ├── screenshot.txt ├── index.txt ├── quick_start.txt ├── changelog └── reference.txt ├── setup.cfg ├── MANIFEST.in ├── po ├── POTFILES.in ├── es.po ├── fr.po └── de.po ├── watchrc.rb ├── .gitignore ├── tyrs ├── README.md └── setup.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/tyrs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/tyrs/shorter/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/reference-dev.txt: -------------------------------------------------------------------------------- 1 | please read the user-guide instead 2 | -------------------------------------------------------------------------------- /doc/tyrs.1.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nic0/tyrs/HEAD/doc/tyrs.1.gz -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build] 2 | i18n = True 3 | 4 | [build_i18n] 5 | domain = tyrs 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include po *.po *.in 2 | recursive-include doc *.txt LICENCE tyrs.1.gz 3 | include README.md 4 | -------------------------------------------------------------------------------- /po/POTFILES.in: -------------------------------------------------------------------------------- 1 | src/tyrs/message.py 2 | src/tyrs/config.py 3 | src/tyrs/editor.py 4 | src/tyrs/help.py 5 | src/tyrs/interface.py 6 | -------------------------------------------------------------------------------- /watchrc.rb: -------------------------------------------------------------------------------- 1 | # usage/ watchr watchrc.rb 2 | 3 | watch '^src/tyrs/(.*)\.py$' do |match| 4 | pychecker match[0] 5 | end 6 | 7 | def pychecker file 8 | system("pychecker --stdlib #{file}") if File.exists?(file) 9 | end 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | *.swo 4 | *~ 5 | *\#*\# 6 | 7 | doc/html 8 | doc/latex 9 | doc/*.html 10 | doc/css 11 | doc/javascripts 12 | doc/*.conf 13 | doc/images/* 14 | 15 | po/*.pot 16 | 17 | tags 18 | build 19 | start 20 | dist 21 | tyrs.egg-info 22 | build.sh 23 | PKGBUILD 24 | src/tyrs/tyrs 25 | -------------------------------------------------------------------------------- /tyrs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Author: Nicolas Paris 5 | # License: GPL 6 | 7 | """ 8 | """ 9 | import gettext 10 | gettext.install('tyrs', unicode=1) 11 | from tyrs.tyrs import main 12 | #import cProfile 13 | 14 | if __name__=="__main__": 15 | #cProfile.run('main()') 16 | main() 17 | -------------------------------------------------------------------------------- /doc/build_site.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | VERS="0.6.2" 4 | DEV="0.6.3-dev" 5 | DATE="2011-12-23" 6 | EMAIL="nicolas.caen@gmail.com" 7 | 8 | HTML="asciidoc \ 9 | --conf-file=layout1.conf \ 10 | --attribute stylesdir=css \ 11 | --attribute iconsdir=images/icons \ 12 | --attribute scriptsdir=javascripts \ 13 | --backend=xhtml11 \ 14 | --attribute icons \ 15 | --attribute=date=$DATE \ 16 | --attribute=email=$EMAIL" 17 | 18 | 19 | $HTML --attribute=revision=$VERS --attribute=devel=$DEV \ 20 | --attribute=date=$DATE index.txt 21 | $HTML --attribute=revision=$VERS quick_start.txt 22 | $HTML --attribute=revision=$VERS -a toc -a numbered reference.txt 23 | $HTML --attribute=revision=$VERS screenshot.txt 24 | -------------------------------------------------------------------------------- /src/tyrs/shorter/custom.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | from urlshorter import UrlShorter 17 | 18 | class CustomUrlShorter(UrlShorter): 19 | def __init__(self): 20 | pass 21 | 22 | def do_shorter(self, longurl): 23 | '''You need from the longurl return a short url''' 24 | pass 25 | -------------------------------------------------------------------------------- /src/tyrs/shorter/urlshorter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import urllib2 17 | 18 | class UrlShorter(object): 19 | 20 | def _quote_url(self, url): 21 | long_url = urllib2.quote(url) 22 | long_url = long_url.replace('/', '%2F') 23 | return long_url 24 | 25 | def _get_request(self, url, data=None): 26 | return urllib2.urlopen(url, data).read() 27 | -------------------------------------------------------------------------------- /src/tyrs/container.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | class Container(object): 17 | ''' 18 | Contain main classes that we need thought all the programm 19 | such as conf, api and ui 20 | ''' 21 | _container = {} 22 | 23 | def __setitem__(self, key, value): 24 | self._container[key] = value 25 | 26 | def __getitem__(self, key): 27 | return self._container[key] 28 | 29 | def add(self, name, dependency): 30 | self[name] = dependency 31 | -------------------------------------------------------------------------------- /src/tyrs/shorter/ur1ca.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | 17 | import re 18 | import urllib 19 | from urlshorter import UrlShorter 20 | 21 | class Ur1caUrlShorter(UrlShorter): 22 | def __init__(self): 23 | self.base = "http://ur1.ca" 24 | self.pt = re.compile('

Your ur1 is: ') 25 | 26 | def do_shorter(self, longurl): 27 | values = {'submit' : 'Make it an ur1!', 'longurl' : longurl} 28 | 29 | data = urllib.urlencode(values) 30 | resp = self._get_request(self.base, data) 31 | short = self.pt.findall(resp) 32 | 33 | if len(short) > 0: 34 | return short[0] 35 | -------------------------------------------------------------------------------- /src/tyrs/shorter/bitly.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import urllib2 17 | try: 18 | import json 19 | except: 20 | import simplejson as json 21 | 22 | from urlshorter import UrlShorter 23 | 24 | APIKEY = 'apiKey=R_f806c2011339080ea0b623959bb8ecff' 25 | VERION = 'version=2.0.1' 26 | LOGIN = 'login=tyrs' 27 | 28 | class BitLyUrlShorter(UrlShorter): 29 | 30 | def __init__(self): 31 | self.base = 'http://api.bit.ly/shorten?%s&%s&%s&longUrl=%s' 32 | 33 | def do_shorter(self, url): 34 | long_url = self._quote_url(url) 35 | request = self.base % (VERION, LOGIN, APIKEY, long_url) 36 | response = json.loads(urllib2.urlopen(request).read()) 37 | return response['results'][url]['shortUrl'] 38 | -------------------------------------------------------------------------------- /src/tyrs/shorter/msudpl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # Copyright © 2011 Natal Ngétal 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # any later version. 8 | 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import re 18 | import urllib 19 | from urlshorter import UrlShorter 20 | 21 | class MsudplUrlShorter(UrlShorter): 22 | def __init__(self): 23 | self.base = "http://msud.pl" 24 | self.pt = re.compile('

Whouah ! This a very beautiful url :\) ') 25 | self.pt_yet_in_base = re.compile('and whouah! It\'s very beautiful ') 26 | 27 | def do_shorter(self, longurl): 28 | values = {'submit' : 'Generate my sexy url', 'sexy_url': longurl} 29 | 30 | data = urllib.urlencode(values) 31 | resp = self._get_request(self.base, data) 32 | short = self.pt.findall(resp) 33 | if len(short) == 0: 34 | short = self.pt_yet_in_base.findall(resp) 35 | 36 | if len(short) > 0: 37 | return self.base + '/' + short[0] 38 | -------------------------------------------------------------------------------- /src/tyrs/completion.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | class Completion (object): 17 | 18 | def __init__(self): 19 | self.nicks = [] 20 | 21 | def add(self, nick): 22 | if nick not in self.nicks: 23 | self.nicks.append(nick) 24 | 25 | def __repr__(self): 26 | return str(self.nicks) 27 | 28 | def __len__(self): 29 | return len(self.nicks) 30 | 31 | def complete(self, word): 32 | nick = [] 33 | for n in self.nicks: 34 | if word in n: 35 | nick.append(n) 36 | if len(nick) is 1: 37 | return nick[0] 38 | else: 39 | return None 40 | 41 | def text_complete(self, text): 42 | """Return the text to insert""" 43 | t = text.split(' ') 44 | last = t[-1] 45 | if last[0] is '@': 46 | nick = self.complete(last[1:]) 47 | if nick: 48 | return nick[len(last)-1:] 49 | return None 50 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import unittest 17 | import sys 18 | sys.path.append('../src/tyrs') 19 | #import gettext 20 | #gettext.install('tyrs', unicode=1) 21 | #import src.utils as utils 22 | import utils 23 | 24 | class TestUtils(unittest.TestCase): 25 | 26 | def setUp(self): 27 | pass 28 | 29 | def tearDown(self): 30 | pass 31 | 32 | def test_cut_attag(self): 33 | nick = '@mynick' 34 | result = utils.cut_attag(nick) 35 | self.assertEqual(result, 'mynick') 36 | 37 | def test_get_source(self): 38 | source = 'tyrs' 39 | result = utils.get_source(source) 40 | self.assertEqual(result, 'tyrs') 41 | 42 | def test_get_exact_nick(self): 43 | nick = ['@mynick', '@mynick,', '@mynick!!', 'mynick,'] 44 | for n in nick: 45 | result = utils.get_exact_nick(n) 46 | self.assertEqual(result, 'mynick') 47 | 48 | if __name__ == '__main__': 49 | unittest.main () 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Description 2 | ----------- 3 | 4 | Tyrs is a twitter and identi.ca client using curses that is easily configurable. 5 | 6 | - [Home Site](http://tyrs.nicosphere.net) 7 | - [Quick Start](http://tyrs.nicosphere.net/quick_start.html) 8 | - [Screenshot](http://tyrs.nicosphere.net/screenshot.html) 9 | - [Reference guide](http://tyrs.nicosphere.net/reference.html) 10 | - [Pypi download page](http://pypi.python.org/pypi/tyrs) 11 | 12 | Functionality 13 | -------------- 14 | 15 | - Identi.ca and Twitter support 16 | - Oauth authentication 17 | - Proxy support 18 | - utf-8 support, 256 Colors, transparency 19 | - Mentions, Direct messages, Favorites, User and Search timelines 20 | - Follow/Unfollow 21 | - Display thread view of replies 22 | - ur1.ca, bit.ly and goo.gl support 23 | - Filter system 24 | - Configuration file allows for lots of customization 25 | - Auto-Completion with nicks 26 | 27 | Installation 28 | ------------ 29 | 30 | * Current release with `easy_install` (for python2.7, but other release coming soon): 31 | 32 | sudo easy_install http://pypi.python.org/packages/2.7/t/tyrs/tyrs-0.5.0-py2.7.egg 33 | 34 | or with python-pip tools: 35 | 36 | sudo pip install tyrs 37 | 38 | Dependecies that should be install automatically: 39 | * python-twitter, python-oauth2, python-argparse 40 | Requires: python-setuptools 41 | 42 | git clone git://github.com/Nic0/tyrs.git 43 | cd tyrs 44 | python setup.py build 45 | sudo python setup.py install 46 | 47 | check the [installation guide](http://tyrs.nicosphere.net/reference.html#installation) for more information. 48 | 49 | Licence 50 | ------- 51 | 52 | GNU GENERAL PUBLIC LICENCE (GPL) 53 | Check `doc/LICENCE` for the full licence text. 54 | 55 | Author 56 | ------ 57 | 58 | Nicolas Paris 59 | https://nsirap.com 60 | -------------------------------------------------------------------------------- /doc/screenshot.txt: -------------------------------------------------------------------------------- 1 | Screenshot of Tyrs with configuration 2 | ===================================== 3 | 4 | [NOTE] 5 | If you've designed a nice looking theme, please send me a screenshot with the 6 | configuration file. I will add some more screenshots to this section soon. 7 | 8 | Default Values Screenshot 9 | ------------------------- 10 | 11 | image:images/tyrs0.3.3.png[] 12 | 13 | Contributions 14 | ------------- 15 | 16 | * Jasonwryan 17 | 18 | Thanks to link:http://jasonwryan.com/[jasonwryan] for sending me this really 19 | nice looking screenshot, using Tyrs 0.3.3. His screenshot is under creative 20 | commons licence (CC-BY 2.0). 21 | 22 | image:images/jasonwryan-tyrs-0.3.3.png[] 23 | 24 | Borderless 25 | ---------- 26 | 27 | image:images/tyrs0.3.3-borderless.png[] 28 | 29 | Blue Theme 30 | ---------- 31 | 32 | (old version of tyrs) 33 | image:images/darkblue.png[] 34 | .Blue Theme Configuration 35 | [source,conf] 36 | ------------------------- 37 | [params] 38 | tweet_border = 0 39 | relative_time = 1 40 | 41 | [colors] 42 | hashtag = 5 43 | text = 4 44 | attag = 3 45 | 46 | color_set3 = 800 800 1000 47 | color_set4 = 600 600 1000 48 | color_set5 = 950 800 950 49 | color_set6 = 600 0 600 50 | 51 | ------------------------- 52 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | 4 | import os 5 | import sys 6 | from distutils.core import setup 7 | 8 | try: 9 | import setuptools 10 | except ImportError: 11 | print 'The installation require setuptools, please install it \ 12 | (python-setuptools or python-distribute)' 13 | 14 | try: 15 | from DistUtilsExtra.command import * 16 | except ImportError: 17 | print 'The installation require python-distutils-extra (apt-get install python-distutils-extra)' 18 | print 'Or' 19 | print 'pip install http://launchpad.net/python-distutils-extra/trunk/2.28/+download/python-distutils-extra-2.28.tar.gz' 20 | 21 | def read(fname): 22 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 23 | 24 | setup(name='tyrs', 25 | version='0.6.2', 26 | description='Twitter and Identica client using curses', 27 | author='Nicolas Paris', 28 | author_email='nicolas.caen@gmail.com', 29 | url='http://tyrs.nicosphere.net', 30 | license='GPLv3', 31 | long_description=read('README.md'), 32 | install_requires=[ 33 | 'python-twitter>=0.8.2', 34 | 'argparse', 35 | 'httplib2==0.6.0', 36 | 'urwid', 37 | ], 38 | packages=['tyrs', 'tyrs.shorter'], 39 | package_dir={'tyrs':'src/tyrs'}, 40 | scripts=['tyrs'], 41 | platforms=['linux'], 42 | classifiers = [ 43 | 'Development Status :: 4 - Beta', 44 | 'Environment :: Console :: Curses', 45 | 'Intended Audience :: End Users/Desktop', 46 | 'License :: OSI Approved :: GNU General Public License (GPL)', 47 | 'Natural Language :: English', 48 | 'Operating System :: POSIX :: Linux', 49 | 'Programming Language :: Python :: 2', 50 | ], 51 | cmdclass = { "build" : build_extra.build_extra, 52 | "build_i18n" : build_i18n.build_i18n, 53 | } 54 | ) 55 | -------------------------------------------------------------------------------- /src/tyrs/update.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import tyrs 17 | import time 18 | import logging 19 | import threading 20 | 21 | class UpdateThread(threading.Thread): 22 | ''' 23 | The only thread that update all timelines 24 | ''' 25 | 26 | def __init__(self): 27 | self.interface = tyrs.container['interface'] 28 | self.conf = tyrs.container['conf'] 29 | self.api = tyrs.container['api'] 30 | threading.Thread.__init__(self, target=self.run) 31 | self._stopevent = threading.Event() 32 | 33 | def run(self): 34 | self.update_timeline() 35 | logging.info('Thread started') 36 | for i in range(self.conf.params['refresh'] * 60): 37 | time.sleep(1) 38 | if self._stopevent.isSet() or self.interface.stoped: 39 | logging.info('Thread forced to stop') 40 | return 41 | self.start_new_thread() 42 | logging.info('Thread stoped') 43 | self._Thread__stop() 44 | 45 | def stop(self): 46 | self._stopevent.set() 47 | 48 | def start_new_thread(self): 49 | update = UpdateThread() 50 | update.start() 51 | 52 | def update_timeline(self): 53 | while not self.interface.loop.screen._started: 54 | time.sleep(1) 55 | timeline = ('home', 'mentions', 'direct') 56 | for t in timeline: 57 | self.api.update_timeline(t) 58 | self.interface.display_timeline() 59 | self.interface.redraw_screen() 60 | -------------------------------------------------------------------------------- /tests/test_shortener.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # Copyright © 2011 Nicolas Paris 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import sys 18 | import unittest 19 | sys.path.append('../src/tyrs') 20 | import random 21 | from shorter.urlshorter import UrlShorter 22 | from shorter.bitly import BitLyUrlShorter 23 | #TODO this shortener has dependencies such as `apiclient` and `urllib3` 24 | #from shorter.googl import GooglUrlShorter 25 | #FIXME msud.pl raises 502 HTTP errors very often 26 | #from shorter.msudpl import MsudplUrlShorter 27 | from shorter.ur1ca import Ur1caUrlShorter 28 | 29 | url_re = 'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+' 30 | 31 | class TestShortener(unittest.TestCase): 32 | 33 | shorteners = [BitLyUrlShorter, Ur1caUrlShorter] 34 | 35 | def shortener_test(self, cls, url): 36 | """ 37 | Receives a class descendant of `UrlShorter` and tests it with the 38 | given url. 39 | """ 40 | assert issubclass(cls, UrlShorter) 41 | shortener = cls() 42 | result = shortener.do_shorter(url) 43 | self.assertRegexpMatches(result, url_re) 44 | 45 | def test_yet_in_base(self): 46 | url = 'http://www.nicosphere.net' 47 | for shortener in self.shorteners: 48 | self.shortener_test(shortener, url) 49 | 50 | def test_random_url(self): 51 | number = random.randint(10000, 100000) 52 | url = 'http://www.nicosphere{0}.net'.format(number) 53 | for shortener in self.shorteners: 54 | self.shortener_test(shortener, url) 55 | 56 | 57 | if __name__ == '__main__': 58 | unittest.main() 59 | -------------------------------------------------------------------------------- /src/tyrs/filter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import re 17 | import tyrs 18 | from utils import get_urls 19 | 20 | class FilterStatus(object): 21 | 22 | def __init__(self): 23 | self.conf = tyrs.container['conf'] 24 | 25 | def filter_status(self, status): 26 | self.setup_exception() 27 | try: 28 | if self.conf.filter['activate']: 29 | self.status = status 30 | if self.filter_without_url(): 31 | if self.filter_without_myself(): 32 | if self.filter_exception(): 33 | return True 34 | return False 35 | except: 36 | return False 37 | 38 | def filter_without_url(self): 39 | urls = get_urls(self.status.text) 40 | if len(urls) == 0: 41 | return True 42 | return False 43 | 44 | def filter_without_myself(self): 45 | if self.conf.filter['myself']: 46 | return True 47 | if self.conf.my_nick in self.status.text: 48 | return False 49 | else: 50 | return True 51 | 52 | 53 | def filter_exception(self): 54 | nick = self.status.user.screen_name 55 | if self.conf.filter['behavior'] == 'all': 56 | if not nick in self.exception: 57 | return True 58 | else: 59 | if nick in self.exception: 60 | return True 61 | return False 62 | 63 | def setup_exception(self): 64 | self.exception = self.conf.filter['except'] 65 | self.exception.append(self.conf.my_nick) 66 | -------------------------------------------------------------------------------- /src/tyrs/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import tyrs 17 | import curses 18 | from utils import encode 19 | 20 | class User(object): 21 | 22 | def __init__(self, user): 23 | self.interface = tyrs.container['interface'] 24 | self.user = user 25 | self.interface.refresh_token = True 26 | self._init_screen() 27 | self._display_header() 28 | self._display_info() 29 | self.screen.getch() 30 | self.screen.erase() 31 | self.interface.refresh_token = False 32 | 33 | def _init_screen(self): 34 | maxyx = self.interface.screen.getmaxyx() 35 | self.screen = self.interface.screen.subwin(30, 80, 3, 10) 36 | self.screen.border(0) 37 | self.screen.refresh() 38 | 39 | def _display_header(self): 40 | self.screen.addstr(2, 10, '%s -- %s' % (self.user.screen_name, 41 | encode(self.user.name))) 42 | 43 | def _display_info(self): 44 | info = { 45 | 'location': encode(self.user.location), 46 | 'description': encode(self.user.description), 47 | 'url': encode(self.user.url), 48 | 'time zone': encode(self.user.time_zone), 49 | 'status': self.user.status, 50 | 'friends': self.user.friends_count, 51 | 'follower': self.user.followers_count, 52 | 'tweets': self.user.statuses_count, 53 | 'verified': self.user.verified, 54 | 'created at': self.user.created_at, 55 | } 56 | i=0 57 | for item in info: 58 | self.screen.addstr(4+i, 5, '%s' % item) 59 | self.screen.addstr(4+i, 20, '%s' % info[item]) 60 | i += 1 61 | 62 | 63 | -------------------------------------------------------------------------------- /doc/index.txt: -------------------------------------------------------------------------------- 1 | Tyrs: easily configured ncurses microblogging client 2 | ===================================================== 3 | 4 | * *Current version* {revision}, Release the {date} 5 | * *Devel version* {devel} 6 | 7 | image:images/tyrs0.3.3.png["Tyrs screenshot", width=128, link="images/tyrs0.3.3.png"] 8 | 9 | Download link:http://pypi.python.org/packages/source/t/tyrs/tyrs-{revision}.tar.gz[the last 10 | version of Tyrs]. + 11 | Get the latest development version on link:http://github.com/Nic0/tyrs[github]. + 12 | This is free (as in freedom) software, released under the GNU General Public 13 | Licence v3. 14 | Download latest version, or easy to install egg in the 15 | link:http://pypi.python.org/pypi/tyrs[python website]. 16 | 17 | If it's your first visit, you might like to check out some 18 | link:screenshot.html[screenshots], or get started with the 19 | link:quick_start.html[Tyrs quick start guide]. 20 | 21 | You can use the link:reference.html#installation[installation guide] to 22 | get Tyrs up and running. 23 | 24 | Features 25 | -------- 26 | 27 | A list of some of the key features: 28 | 29 | * Supports Identi.ca and Twitter services 30 | * OAuth authentication. 31 | * UTF-8 support. 32 | * User Interface with Urwid 33 | * Tweets, Retweets, Follow, Unfollow. 34 | * Mentions, Replies 35 | * Threaded view of replies 36 | * Search 37 | * Direct messages 38 | * Transparency and colors. 39 | * Based on ncurses. 40 | * Customization of keys binding. 41 | * Customization of colors. 42 | * Easily configurable, readable configuration. 43 | * Opens URL with just single key. 44 | * ur1.ca and bit.ly support for url shorter. 45 | 46 | [IMPORTANT] 47 | This project is relatively new, having started in May 2011, and currently in 48 | heavy development. There is lots of planned functionality still to be coded. 49 | 50 | Helpfull links 51 | -------------- 52 | 53 | * Ask any questions, or just follow latest news on the 54 | link:http://groups.google.com/group/tyrs[mailing list]. 55 | * Send me an email at . 56 | * Check the development on link:http://github.com/Nic0/tyrs[github]. 57 | * If you like Tyrs, you may send a quick hello to 58 | link:http://twitter.com/nic0sphere[my twitter account `@nic0sphere`], or use 59 | the '#Tyrs' tag. 60 | * You can catch me on freenode server, #tyrs -- recently opened 61 | * link:http://www.nicosphere.net[My blog], mostly in French. 62 | 63 | (C) Copyright 2011 ~ Nicolas Paris. 64 | -------------------------------------------------------------------------------- /tests/test_twitter_api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import unittest 17 | import sys 18 | from time import gmtime, strftime 19 | import gettext 20 | gettext.install('tyrs', unicode=1) 21 | 22 | sys.path.append('../src') 23 | from tyrs import tyrs, tweets 24 | from twitter import TwitterError, Status, User 25 | 26 | class TestTwitterApi(unittest.TestCase): 27 | 28 | def setUp(self): 29 | self.authenticate() 30 | 31 | def authenticate(self): 32 | #TODO use twitturse credentials 33 | tyrs.init_conf() 34 | tyrs.init_timelines() 35 | tyrs.init_api() 36 | self.api = tweets.Tweets() 37 | self.api.authentication() 38 | 39 | def test_authentication(self): 40 | myself = self.api.myself 41 | username = myself.screen_name 42 | self.assertIsInstance(myself, User) 43 | self.assertEqual(username, 'twitturse') 44 | 45 | def test_post_update(self): 46 | tweet = 'test from unittest at ' + self.get_time() 47 | result = self.api.post_tweet(tweet) 48 | self.assertEqual(result.text, tweet) 49 | self.assertIsInstance(result, Status) 50 | 51 | def test_post_empty_update(self): 52 | tweet = '' 53 | result = self.api.post_tweet(tweet) 54 | self.assertIsNone(result) 55 | 56 | #FIXME! `Tweets` hasn't got an `update_home_timeline` method 57 | #def test_update_home_timeline(self): 58 | #result = self.api.update_home_timeline() 59 | #self.assertIsInstance(result[0], Status) 60 | #self.assertIsInstance(result[10], Status) 61 | 62 | def get_time(self): 63 | return strftime('%H:%M:%S', gmtime()) 64 | 65 | 66 | if __name__ == '__main__': 67 | unittest.main () 68 | -------------------------------------------------------------------------------- /tests/test_complete.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import sys 17 | import unittest 18 | sys.path.append('../src/tyrs') 19 | from completion import Completion 20 | 21 | class TestCompletion(unittest.TestCase): 22 | 23 | def test_class(self): 24 | nicks = Completion() 25 | self.assertIsInstance(nicks, Completion) 26 | 27 | 28 | def test_add(self): 29 | nicks = Completion() 30 | nicks.add('coin') 31 | self.assertEqual(1, len(nicks)) 32 | nicks.add('pan') 33 | self.assertEqual(2, len(nicks)) 34 | 35 | def test_add_existing(self, ): 36 | nicks = Completion() 37 | nicks.add('coin') 38 | nicks.add('coin') 39 | self.assertEqual(1, len(nicks)) 40 | 41 | def test_return_completion(self): 42 | nicks = Completion() 43 | nicks.add('coincoin') 44 | nicks.add('cooooooo') 45 | result = nicks.complete('coi') 46 | self.assertEqual('coincoin', result) 47 | result = nicks.complete('pan') 48 | self.assertIsNone(result) 49 | result = nicks.complete('co') 50 | self.assertIsNone(result) 51 | 52 | def test_return_text_completed(self): 53 | nicks = Completion() 54 | nicks.add('coin') 55 | nicks.add('pan') 56 | text = "foo bar @co" 57 | result = nicks.text_complete(text) 58 | self.assertEqual(result, 'in') 59 | 60 | def test_return_text_completed_failed(self): 61 | nicks = Completion() 62 | nicks.add('coin') 63 | nicks.add('pan') 64 | text = ['foo bar co', 'foo @co bar'] 65 | for t in text: 66 | result = nicks.text_complete(t) 67 | self.assertIsNone(result) 68 | 69 | if __name__ == '__main__': 70 | unittest.main () 71 | -------------------------------------------------------------------------------- /src/tyrs/shorter/googl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import os 17 | import sys 18 | import httplib2 19 | from urlshorter import UrlShorter 20 | 21 | from apiclient.discovery import build 22 | 23 | from oauth2client.file import Storage 24 | from oauth2client.client import AccessTokenRefreshError 25 | from oauth2client.client import OAuth2WebServerFlow 26 | from oauth2client.tools import run 27 | 28 | FLOW = OAuth2WebServerFlow( 29 | client_id='382344260739.apps.googleusercontent.com', 30 | client_secret='fJwAFxKWyW4rBmzzm6V3TVsZ', 31 | scope='https://www.googleapis.com/auth/urlshortener', 32 | user_agent='urlshortener-tyrs/1.0') 33 | 34 | googl_token_file = os.environ['HOME'] + '/.config/tyrs/googl.tok' 35 | 36 | class GooglUrlShorter(UrlShorter): 37 | 38 | def do_shorter(self, longurl): 39 | 40 | storage = Storage(googl_token_file) 41 | credentials = storage.get() 42 | if credentials is None or credentials.invalid: 43 | return 'need to register to use goog.gl' 44 | 45 | http = httplib2.Http() 46 | http = credentials.authorize(http) 47 | 48 | service = build("urlshortener", "v1", http=http) 49 | 50 | try: 51 | 52 | url = service.url() 53 | 54 | body = {"longUrl": longurl } 55 | resp = url.insert(body=body).execute() 56 | 57 | return resp['id'] 58 | 59 | except AccessTokenRefreshError: 60 | pass 61 | 62 | def register_token(self): 63 | storage = Storage(googl_token_file) 64 | credentials = storage.get() 65 | if credentials is None or credentials.invalid: 66 | print 'There is no token file found for goo.gl' 67 | print 'A file will be generated for you' 68 | credentials = run(FLOW, storage) 69 | -------------------------------------------------------------------------------- /src/tyrs/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import re 17 | import os 18 | import sys 19 | #import tyrs 20 | import string 21 | from htmlentitydefs import entitydefs 22 | 23 | def set_console_title(): 24 | try: 25 | sys.stdout.write("\x1b]2;Tyrs\x07") 26 | except: 27 | pass 28 | 29 | def cut_attag(name): 30 | if name[0] == '@': 31 | name = name[1:] 32 | return name 33 | 34 | def get_exact_nick(word): 35 | if word[0] == '@': 36 | word = word[1:] 37 | alphanum = string.letters + string.digits 38 | try: 39 | while word[-1] not in alphanum: 40 | word = word[:-1] 41 | except IndexError: 42 | pass 43 | return word 44 | 45 | def encode(string): 46 | try: 47 | return string.encode(sys.stdout.encoding, 'replace') 48 | except AttributeError: 49 | return string 50 | 51 | def html_unescape(str): 52 | """ Unescapes HTML entities """ 53 | def entity_replacer(m): 54 | entity = m.group(1) 55 | if entity in entitydefs: 56 | return entitydefs[entity] 57 | else: 58 | return m.group(0) 59 | 60 | return re.sub(r'&([^;]+);', entity_replacer, str) 61 | 62 | 63 | def get_source(source): 64 | if source != 'web': 65 | source = source.split('>') 66 | source = source[1:] 67 | source = ' '.join(source) 68 | source = source.split('<')[:1] 69 | source = source[:1] 70 | source = ' '.join(source) 71 | return source 72 | 73 | def open_image(user): 74 | image = user.profile_image_url 75 | command = tyrs.container['conf'].params['open_image_command'] 76 | os.system(command % image) 77 | 78 | 79 | def get_urls(text): 80 | return re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', text) 81 | -------------------------------------------------------------------------------- /src/tyrs/tyrs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright © 2011 Nicolas Paris 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | ''' 18 | Tyrs 19 | 20 | @author: Nicolas Paris 21 | @date: 23/12/2011 22 | @licence: GPLv3 23 | 24 | 25 | ''' 26 | 27 | __revision__ = "0.6.2" 28 | 29 | import sys 30 | import utils 31 | import config 32 | import locale 33 | import tweets 34 | import argparse 35 | import gettext 36 | from urllib2 import URLError 37 | from timeline import Timeline 38 | from container import Container 39 | from interface import Interface 40 | from completion import Completion 41 | 42 | locale.setlocale(locale.LC_ALL, '') 43 | container = Container() 44 | 45 | def arguments(): 46 | ''' 47 | Parse all arguments from the CLI 48 | ''' 49 | parser = argparse.ArgumentParser( 50 | 'Tyrs: a twitter client writen in python with curses.') 51 | parser.add_argument('-a', '--account', 52 | help='Use another account, store in a different file.') 53 | parser.add_argument('-c', '--config', 54 | help='Use another configuration file.') 55 | parser.add_argument('-g', '--generate-config', 56 | help='Generate a default configuration file.') 57 | parser.add_argument('-v', '--version', action='version', version='Tyrs %s' % __revision__, 58 | help='Show the current version of Tyrs') 59 | args = parser.parse_args() 60 | return args 61 | 62 | def main(): 63 | 64 | utils.set_console_title() 65 | init_conf() 66 | init_tyrs() 67 | 68 | def init_tyrs(): 69 | init_timelines() 70 | init_api() 71 | init_interface() 72 | 73 | def init_conf(): 74 | conf = config.Config(arguments()) 75 | container.add('conf', conf) 76 | 77 | 78 | def init_api(): 79 | api = tweets.Tweets() 80 | container.add('api', api) 81 | try: 82 | api.authentication() 83 | except URLError, e: 84 | print 'error:%s' % e 85 | sys.exit(1) 86 | 87 | def init_interface(): 88 | user_interface = Interface() 89 | container.add('interface', user_interface) 90 | 91 | def init_timelines(): 92 | buffers = ( 93 | 'home', 'mentions', 'direct', 'search', 94 | 'user', 'favorite', 'thread', 'user_retweet', 95 | 'list' 96 | ) 97 | timelines = {} 98 | for buff in buffers: 99 | timelines[buff] = Timeline(buff) 100 | container.add('timelines', timelines) 101 | container.add('buffers', buffers) 102 | completion = Completion() 103 | container.add('completion', completion) 104 | 105 | if __name__ == "__main__": 106 | gettext.install('tyrs', unicode=1) 107 | main() 108 | -------------------------------------------------------------------------------- /src/tyrs/editor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import tyrs 17 | import urwid 18 | from utils import encode, get_urls 19 | try: 20 | from shorter.ur1ca import Ur1caUrlShorter 21 | from shorter.bitly import BitLyUrlShorter 22 | from shorter.msudpl import MsudplUrlShorter 23 | from shorter.custom import CustomUrlShorter 24 | except ImportError: 25 | pass 26 | 27 | try: 28 | from shorter.googl import GooglUrlShorter 29 | except ImportError: 30 | pass 31 | 32 | class TweetEditor(urwid.WidgetWrap): 33 | 34 | __metaclass__ = urwid.signals.MetaSignals 35 | signals = ['done'] 36 | 37 | def __init__(self, init_content='', prompt=''): 38 | if init_content: 39 | init_content += ' ' 40 | self.editor = Editor(u'%s (twice enter key to validate or esc) \n>> ' % prompt, init_content) 41 | self.counter = urwid.Text('0') 42 | self.editor.completion = tyrs.container['completion'] 43 | w = urwid.Columns([ ('fixed', 4, self.counter), self.editor]) 44 | urwid.connect_signal(self.editor, 'done', self.send_sigterm) 45 | urwid.connect_signal(self.editor, 'change', self.update_count) 46 | 47 | self.__super.__init__(w) 48 | 49 | def send_sigterm(self, content): 50 | urwid.emit_signal(self, 'done', content) 51 | 52 | def update_count(self, edit, new_edit_text): 53 | self.counter.set_text(str(len(new_edit_text))) 54 | 55 | class Editor(urwid.Edit): 56 | 57 | __metaclass__ = urwid.signals.MetaSignals 58 | signals = ['done'] 59 | last_key = '' 60 | 61 | def keypress(self, size, key): 62 | if key == 'enter' and self.last_key == 'enter': 63 | urwid.emit_signal(self, 'done', self.get_edit_text()) 64 | return 65 | if key == 'esc': 66 | urwid.emit_signal(self, 'done', None) 67 | if key == 'tab': 68 | insert_text = self.completion.text_complete(self.get_edit_text()) 69 | if insert_text: 70 | self.insert_text(insert_text) 71 | 72 | self.last_key = key 73 | urwid.Edit.keypress(self, size, key) 74 | 75 | #FIXME old editor, need to be done for url-shorter 76 | 77 | #def shorter_url(self): 78 | #self._set_service() 79 | #long_urls = get_urls(self.content) 80 | #for long_url in long_urls: 81 | #short_url = self.shorter.do_shorter(long_url) 82 | #try: 83 | #self.content = self.content.replace(long_url, short_url) 84 | #except UnicodeDecodeError: 85 | #pass 86 | 87 | #def _set_service(self): 88 | #service = self.conf.params['url_shorter'] 89 | #if service == 'bitly': 90 | #self.shorter = BitLyUrlShorter() 91 | #elif service == 'googl': 92 | #self.shorter = GooglUrlShorter() 93 | #elif service == 'msudpl': 94 | #self.shorter = MsudplUrlShorter() 95 | #elif service == 'custom': 96 | #self.shorter = CustomUrlShorter() 97 | #else: 98 | #self.shorter = Ur1caUrlShorter() 99 | -------------------------------------------------------------------------------- /src/tyrs/message.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | from utils import encode 17 | 18 | class FlashMessage(object): 19 | 20 | message = { 21 | 'update': [ 22 | _('Updating timeline...'), 23 | _('Couldn\'t retrieve tweets') 24 | ], 25 | 'tweet': [ 26 | _('Your tweet was sent'), 27 | _('Couldn\'t send tweet'), 28 | ], 29 | 'retweet': [ 30 | _('Your retweet was sent'), 31 | _('Couldn\'t send retweet'), 32 | ], 33 | 'destroy': [ 34 | _('You have deleted the tweet'), 35 | _('Couldn\'t delete tweet'), 36 | ], 37 | 'favorite': [ 38 | _('The tweet was added to favorites list'), 39 | _('Couldn\'t add tweet to favorites list'), 40 | ], 41 | 'favorite_del': [ 42 | _('Tweet was removed from favorites list'), 43 | _('Couldn\'t delete tweet on favorites list'), 44 | ], 45 | 'direct': [ 46 | _('Direct message was sent'), 47 | _('Couldn\'t send direct message'), 48 | ], 49 | 'follow': [ 50 | _('You are now following %s'), 51 | _('Couldn\'t follow %s') 52 | ], 53 | 'unfollow': [ 54 | _('You are not following %s anymore'), 55 | _('Couldn\'t stop following %s') 56 | ], 57 | 'search': [ 58 | _('Search results for %s'), 59 | _('Couldn\'t search for %s'), 60 | ], 61 | 'list': [ 62 | _('List results for %s'), 63 | _('Couldn\'t get list for %s'), 64 | ], 65 | 'empty': [ 66 | '','' 67 | ] 68 | } 69 | 70 | def __init__(self): 71 | self.reset() 72 | 73 | def reset(self): 74 | self.level = 0 75 | self.event = 'empty' 76 | self.string = '' 77 | 78 | def warning(self): 79 | self.level = 1 80 | 81 | def get_msg(self): 82 | return self.compose_msg() 83 | 84 | def compose_msg(self): 85 | try: 86 | msg = self.message[self.event][self.level] % self.string 87 | except TypeError: 88 | msg = self.message[self.event][self.level] 89 | return ' ' +msg+ ' ' 90 | 91 | def print_ask_service(token_file): 92 | print '' 93 | print encode(_('Couldn\'t find any profile.')) 94 | print '' 95 | print encode(_('It should reside in: %s')) % token_file 96 | print encode(_('If you want to setup a new account, then follow these steps')) 97 | print encode(_('If you want to skip this, just press return or ctrl-C.')) 98 | print '' 99 | 100 | print '' 101 | print encode(_('Which service do you want to use?')) 102 | print '' 103 | print '1. Twitter' 104 | print '2. Identi.ca' 105 | print '' 106 | 107 | def print_ask_root_url(): 108 | print '' 109 | print '' 110 | print encode(_('Which root url do you want? (leave blank for default, https://identi.ca/api)')) 111 | print '' 112 | -------------------------------------------------------------------------------- /src/tyrs/constant.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | palette = [ 17 | ['body','default', '', 'standout'], 18 | ['focus','dark red', '', 'standout'], 19 | ['header','light blue', ''], 20 | ['line', 'dark blue', ''], 21 | ['info_msg', 'dark green', ''], 22 | ['warn_msg', 'dark red', ''], 23 | ['current_tab', 'light blue', ''], 24 | ['other_tab', 'dark blue', ''], 25 | ['read', 'dark blue', ''], 26 | ['unread', 'dark red', ''], 27 | ['hashtag', 'dark green', ''], 28 | ['attag', 'brown', ''], 29 | ['highlight', 'dark red', ''], 30 | ['highlight_nick', 'light red', ''], 31 | ['help_bar', 'yellow', 'dark blue'], 32 | ['help_key', 'dark red', ''], 33 | ] 34 | 35 | key = { 36 | 'up': 'k', 37 | 'down': 'j', 38 | 'left': 'J', 39 | 'right': 'K', 40 | 'quit': 'q', 41 | 'tweet': 't', 42 | 'clear': 'c', 43 | 'retweet': 'r', 44 | 'retweet_and_edit': 'R', 45 | 'delete': 'C', 46 | 'update': 'u', 47 | 'follow_selected': 'f', 48 | 'unfollow_selected': 'l', 49 | 'follow': 'F', 50 | 'unfollow': 'L', 51 | 'openurl': 'o', 52 | 'open_image': 'ctrl i', 53 | 'home': 'h', 54 | 'mentions': 'm', 55 | 'reply': 'M', 56 | 'back_on_top': 'g', 57 | 'back_on_bottom': 'G', 58 | 'getDM': 'd', 59 | 'sendDM': 'D', 60 | 'search': 's', 61 | 'search_user': 'U', 62 | 'search_current_user': 'ctrl f', 63 | 'search_myself': 'ctrl u', 64 | 'redraw': 'ctrl l', 65 | 'fav': 'b', 66 | 'get_fav': 'B', 67 | 'delete_fav': 'ctrl b', 68 | 'thread': 'T', 69 | 'waterline': 'w', 70 | 'do_list': 'a', 71 | } 72 | 73 | params = { 74 | 'refresh': 2, 75 | 'tweet_border': 1, 76 | 'relative_time': 1, 77 | 'retweet_by': 1, 78 | 'margin': 1, 79 | 'padding': 2, 80 | 'openurl_command': 'firefox %s', 81 | 'open_image_command': 'feh %s', 82 | 'transparency': True, 83 | 'activities': True, 84 | 'compact': False, 85 | 'help': True, 86 | 'old_skool_border': False, 87 | 'box_position': 1, 88 | 'url_shorter': 'ur1ca', 89 | 'logging_level': 3, 90 | 'header_template': ' {nick}{retweeted}{retweeter} - {time}{reply} {retweet_count} ', 91 | 'proxy': None, 92 | 'beep': False, 93 | } 94 | 95 | filter = { 96 | 'activate': False, 97 | 'myself': False, 98 | 'behavior': 'all', 99 | 'except': [], 100 | } 101 | 102 | token = { 103 | 'twitter': { 104 | 'consumer_key': 'Eq9KLjwH9sJNcpF4OOYNw', 105 | 'consumer_secret': '3JoHyvBp3L6hhJo4BJr6H5aFxLhSlR70ZYnM8jBCQ' 106 | }, 107 | 'identica': { 108 | 'consumer_key': '6b2cf8346a141d530739f72b977b7078', 109 | 'consumer_secret': '31b342b348502345d4a343a331e00edb' 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /doc/quick_start.txt: -------------------------------------------------------------------------------- 1 | Tyrs Quick Start 2 | ================= 3 | :author: Nicolas Paris 4 | 5 | 6 | This is a short introduction to using Tyrs, and doesn't go into 7 | too much detail about configuration or installation. It is intended 8 | to get you up and running Tyrs quickly. 9 | 10 | For more information, you could read these documents: 11 | 12 | * link:reference.html[Full documentation.] 13 | * link:reference.html#installation[The installation guide.] 14 | 15 | First authentication 16 | -------------------- 17 | 18 | Both Twitter and Identi.ca use OAuth for authentication. The first step, with 19 | both services, is to authenticate. 20 | 21 | When you start the application, and it doesn't find a profile file, it will 22 | print: 23 | 24 | .Message when no profile is detected 25 | -------------------- 26 | There is no profile detected. 27 | 28 | The default location for a config file is $XDG_CONFIG_HOME/tyrs/tyrs.cfg which would usually be something like: /home/nicolas/.config/tyrs/tyrs.cfg 29 | 30 | If you want to setup a new account, you are required to complete some basic 31 | steps. 32 | If you want to skip this, just press return or Ctrl-c. 33 | 34 | Which service do you want to use? 35 | 36 | 1. Twitter 37 | 2. Identi.ca 38 | 39 | Requesting temp token from Twitter 40 | 41 | Please visit this Twitter page and retrieve the pincode to be used 42 | in the next step for obtaining an Authentication Token: 43 | 44 | https://api.twitter.com/oauth/authorize?oauth_token=OxFDW4zW3YeiueuizjazAa4VSdeuieuiMO5b8GpS8EoDKRM 45 | 46 | Pincode? 47 | ------------------- 48 | 49 | You only need to go to the link show above, and copy the code that the 50 | Twitter provides. And that's it, you are now able to access Twitter from your console. 51 | 52 | [NOTE] 53 | ==================== 54 | For Identi.ca, one more question will be asked: 55 | Which root url do you want? (leave blank for default value, https://identi.ca/api) 56 | 57 | If you are using identi.ca, leave the default value, and just press return. You 58 | only need to change this if you have your own StatusNet server running. 59 | ==================== 60 | 61 | [TIP] 62 | =================== 63 | If you have several accounts, you can specify which one to use by starting the application with 64 | `-a` argument. 65 | [source,bash] 66 | tyrs -a foo 67 | 68 | =================== 69 | 70 | Usage 71 | ----- 72 | 73 | Tyrs is mostly used with the keyboard, and comes with default key shortcuts (you 74 | may customize these). 75 | Below is a list of the most common ones. You may check 76 | link:/reference.html[the full reference guide] for more details. 77 | 78 | Navigation 79 | ~~~~~~~~~~ 80 | 81 | * *j*: Move down 82 | * *k*: Move up 83 | * arrow keys also allow you to navigate between timelines or tweets 84 | 85 | Functions 86 | ~~~~~~~~~ 87 | 88 | * *t*: Open a box to edit a tweet 89 | * *r*: Retweet the current tweet 90 | * *T*: View thread reply (message will have an envelope next to it) 91 | * *f*: *F*: Follow someone (f, follow the current, F be prompted for who you want follow) 92 | * *l*: *L*: Unfollow (leave) someone 93 | * *q*: Quit 94 | * *c*: Clear the timeline 95 | * *u*: Update the timeline 96 | 97 | Tweet editing 98 | ~~~~~~~~~~~~~ 99 | 100 | * ESC: Abort the edit 101 | * ENTER: Validation/Send 102 | * DEL: delete last character 103 | 104 | 105 | Configuration 106 | ------------- 107 | 108 | You can use a configuration file, although there is no need to have one for Tyrs 109 | to work, where you can customize your keybindings, color and basic behavior. 110 | 111 | The default location for a configuration file is 112 | '$XDG_CONFIG_HOME/tyrs/tyrs.cfg' which would usually be something like: 113 | '/home/nicolas/.config/tyrs/tyrs.cfg' 114 | 115 | To learn how the configuration works, check the 116 | link:reference.html#configuration[configuration part] of the reference guide. 117 | 118 | [NOTE] 119 | An easy way to start with the configuration file, it's to generate one from the 120 | command line interface. It will generate a file with the required parameters 121 | (you can then edit this file to suit your requirements). 122 | [source,bash] 123 | tyrs -g ~/.config/tyrs/tyrs.cfg 124 | 125 | [TIP] 126 | You may have more than one configuration file. To use an account-specific .cfg 127 | file, start with a `-c` argument. (or `--config`) 128 | 129 | Related Sites 130 | ------------- 131 | 132 | * Check the development on link:https://github.com/Nic0/tyrs[Github] 133 | * Found a bug ? or want to propose an idea, check the link:https://github.com/Nic0/tyrs/issues[BugTracker] 134 | * Ask any questions, or just follow latest news on the 135 | link:http://groups.google.com/group/tyrs[mailing list]. 136 | -------------------------------------------------------------------------------- /src/tyrs/timeline.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import urwid 17 | from widget import StatusWidget 18 | from filter import FilterStatus 19 | 20 | class Timeline(object): 21 | 22 | def __init__(self, buffer): 23 | self.cleared = False 24 | self.buffer = buffer 25 | self.walker = [] 26 | self.unread = 0 27 | self.count = 0 28 | self.last_read = 0 29 | self.page = 1 30 | self.filter = FilterStatus() 31 | self.timeline = urwid.ListBox(urwid.SimpleListWalker([])) 32 | 33 | def append_new_statuses(self, retreive): 34 | retreive = self.filter_statuses(retreive) 35 | 36 | if retreive: 37 | self.last_read = retreive[0].id 38 | 39 | if len(self.walker) == 0 and not self.cleared: 40 | self.build_new_walker(retreive) 41 | else: 42 | self.add_to_walker(retreive) 43 | self.add_waterline() 44 | 45 | def add_to_walker(self, retreive): 46 | size = self.interface.loop.screen_size 47 | on_top = 'top' in self.timeline.ends_visible(size) 48 | focus_status, pos = self.walker.get_focus() 49 | for i, status in enumerate(retreive): 50 | # New statuses are insert 51 | if status.id == self.cleared: 52 | return 53 | while status.id != self.walker[0+i].id: 54 | self.walker.insert(i, StatusWidget(status.id, status)) 55 | if on_top: 56 | self.timeline.set_focus(0) 57 | self.timeline.set_focus(pos+i+1) 58 | 59 | # otherwise it just been updated 60 | self.timeline.set_focus(pos) 61 | self.walker[i] = StatusWidget(status.id, status) 62 | 63 | def add_waterline(self): 64 | if self.buffer == 'home' and self.walker[0].id != None: 65 | div = urwid.Divider('-') 66 | div.id = None 67 | self.walker.insert(self.find_waterline(), div) 68 | 69 | def build_new_walker(self, retreive): 70 | items = [] 71 | for i, status in enumerate(retreive): 72 | items.append(StatusWidget(status.id, status)) 73 | self.walker = urwid.SimpleListWalker(items) 74 | self.timeline = urwid.ListBox(self.walker) 75 | import tyrs 76 | self.interface = tyrs.container['interface'] 77 | urwid.connect_signal(self.walker, 'modified', self.interface.lazzy_load) 78 | 79 | def find_waterline(self): 80 | for i, v in enumerate(self.walker): 81 | if str(v.id) == self.interface.last_read_home: 82 | return i 83 | return 0 84 | 85 | def filter_statuses(self, statuses): 86 | filters = [] 87 | for i, status in enumerate(statuses): 88 | if self.filter.filter_status(status): 89 | filters.append(i) 90 | filters.reverse() 91 | for f in filters: 92 | del statuses[f] 93 | 94 | return statuses 95 | 96 | def update_counter(self): 97 | self.count_statuses() 98 | self.count_unread() 99 | 100 | def append_old_statuses(self, statuses): 101 | if statuses == []: 102 | pass 103 | else: 104 | items = [] 105 | for status in statuses: 106 | items.append(StatusWidget(status.id, status)) 107 | self.walker.extend(items) 108 | self.count_statuses() 109 | self.count_unread() 110 | 111 | def count_statuses(self): 112 | try: 113 | self.count = len(self.walker) 114 | except TypeError: 115 | self.count = 0 116 | 117 | def count_unread(self): 118 | try: 119 | self.unread = 0 120 | for i in range(len(self.walker)): 121 | if self.walker[i].id == self.last_read: 122 | break 123 | self.unread += 1 124 | except TypeError: 125 | self.unread = 0 126 | 127 | def reset(self): 128 | self.first = 0 129 | self.unread = 0 130 | 131 | def clear(self): 132 | urwid.disconnect_signal(self.walker, 'modified', self.interface.lazzy_load) 133 | while len(self.walker) > 1: 134 | pop = self.walker.pop() 135 | self.cleared = pop.id 136 | if self.cleared == None: 137 | self.cleared = True 138 | 139 | def empty(self, buffer): 140 | self.__init__(buffer) 141 | 142 | def all_read(self): 143 | if self.count > 0: 144 | self.last_read = self.walker[0].id 145 | 146 | def go_up(self): 147 | focus_status, pos = self.walker.get_focus() 148 | if pos > 0: 149 | self.timeline.set_focus(pos-1) 150 | 151 | def go_down(self): 152 | focus_status, pos = self.walker.get_focus() 153 | self.timeline.set_focus(pos+1) 154 | 155 | def status_count(self): 156 | self.count_statuses() 157 | return self.count 158 | -------------------------------------------------------------------------------- /src/tyrs/help.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import tyrs 17 | import urwid 18 | 19 | def help_bar(): 20 | conf = tyrs.container['conf'] 21 | if conf.params['help']: 22 | return urwid.AttrWrap(urwid.Columns([ 23 | urwid.Text(['help:', ('help_key', ' ? ')]), 24 | urwid.Text(['up:', ('help_key', ' %s ' % conf.keys['up'])]), 25 | urwid.Text(['down:', ('help_key', ' %s ' % conf.keys['down'])]), 26 | urwid.Text(['tweet:', ('help_key', ' %s ' % conf.keys['tweet'])]), 27 | ('fixed', 12, urwid.Text(['retweet:', ('help_key', ' %s ' % 28 | conf.keys['retweet'])])), 29 | urwid.Text(['reply:', ('help_key', ' %s ' % conf.keys['reply'])]), 30 | urwid.Text(['quit:', ('help_key', ' %s ' % conf.keys['quit'])]), 31 | ]), 'help_bar') 32 | else: 33 | return None 34 | 35 | class Help(urwid.WidgetWrap): 36 | 37 | col = [20, 7] 38 | 39 | def __init__ (self): 40 | self.interface = tyrs.container['interface'] 41 | self.conf = tyrs.container['conf'] 42 | self.items = [] 43 | w = urwid.AttrWrap(self.display_help_screen(), 'body') 44 | self.__super.__init__(w) 45 | 46 | def display_help_screen (self): 47 | 48 | self.display_header() 49 | # Navigation 50 | self.display_division(_('Navigation')) 51 | self.display_help_item('up', _('Go up one tweet')) 52 | self.display_help_item('down', _('Go down one tweet')) 53 | self.display_help_item('back_on_top', _('Go to top of screen')) 54 | self.display_help_item('back_on_bottom', _('Go to bottom of screen')) 55 | # Timelines 56 | self.display_division(_('Timelines')) 57 | self.display_help_item('left', _('Go left on the timeline\'s bar')) 58 | self.display_help_item('right', _('Go right on the timeline\'s bar')) 59 | self.display_help_item('update', _('Refresh current timeline')) 60 | self.display_help_item('clear', _('Clear all but last tweet in timeline')) 61 | self.display_help_item('home', _('Go to home timeline')) 62 | self.display_help_item('mentions', _('Go to mentions timeline')) 63 | self.display_help_item('getDM', _('Go to direct message timeline')) 64 | self.display_help_item('search', _('Search for term and show resulting timeline')) 65 | self.display_help_item('search_user', _('Show somebody\'s public timeline')) 66 | self.display_help_item('search_myself', _('Show your public timeline')) 67 | # Tweets 68 | self.display_division(_('Tweets')) 69 | self.display_help_item('tweet', _('Send a tweet')) 70 | self.display_help_item('retweet', _('Retweet selected tweet')) 71 | self.display_help_item('retweet_and_edit', _('Retweet selected tweet, but edit first')) 72 | self.display_help_item('reply', _('Reply to selected tweet')) 73 | self.display_help_item('sendDM', _('Send direct message')) 74 | self.display_help_item('delete', _('Delete selected tweet (must be yours)')) 75 | # Follow/Unfollow 76 | self.display_division('Follow/Unfollow') 77 | self.display_help_item('follow_selected', _('Follow selected twitter')) 78 | self.display_help_item('unfollow_selected', _('Unfollow selected twitter')) 79 | self.display_help_item('follow', _('Follow a twitter')) 80 | self.display_help_item('unfollow', _('Unfollow a twitter')) 81 | 82 | # Favorite 83 | self.display_division('Favorite') 84 | self.display_help_item('fav', _('Bookmark selected tweet')) 85 | self.display_help_item('get_fav', _('Go to favorite timeline')) 86 | self.display_help_item('delete_fav', _('Delete an favorite tweet')) 87 | 88 | # Others 89 | self.display_division(_('Others')) 90 | self.display_help_item('quit', _('Leave Tyrs')) 91 | self.display_help_item('waterline', _('Move the waterline to the top')) 92 | self.display_help_item('openurl', _('Open URL in browser')) 93 | self.display_help_item('open_image', _('Open image in browser')) 94 | self.display_help_item('redraw', _('Redraw the screen')) 95 | self.display_help_item('thread', _('Open thread seltected')) 96 | return urwid.ListBox(urwid.SimpleListWalker(self.items)) 97 | 98 | 99 | def display_division(self, title): 100 | self.items.append(urwid.Divider(' ')) 101 | self.items.append(urwid.Padding(urwid.AttrWrap(urwid.Text(title), 'focus'), left=4)) 102 | self.items.append(urwid.Divider(' ')) 103 | 104 | def display_header(self): 105 | self.items.append( urwid.Columns([ 106 | ('fixed', self.col[0], urwid.Text(' Name')), 107 | ('fixed', self.col[1], urwid.Text('Key')), 108 | urwid.Text('Description') 109 | ])) 110 | 111 | def display_help_item(self, key, description): 112 | self.items.append( urwid.Columns([ 113 | ('fixed', self.col[0], urwid.Text(' '+key)), 114 | ('fixed', self.col[1], urwid.Text(self.conf.keys[key])), 115 | urwid.Text(description) 116 | ])) 117 | -------------------------------------------------------------------------------- /src/tyrs/keys.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import tyrs 17 | import urwid 18 | from help import Help 19 | from utils import open_image 20 | 21 | class Keys(object): 22 | 23 | __metaclass__ = urwid.signals.MetaSignals 24 | signals = ['help_done'] 25 | ''' 26 | This class handle the main keysbinding, as the main method contain every 27 | keybinding, every case match a key to a method call, there is no logical 28 | here 29 | ''' 30 | def __init__(self): 31 | self.conf = tyrs.container['conf'] 32 | self.interface = tyrs.container['interface'] 33 | self.api = tyrs.container['api'] 34 | 35 | def keystroke (self, ch): 36 | if not self.interface.help: 37 | # Quit 38 | if ch == self.conf.keys['quit']: 39 | self.interface.stoped = True 40 | raise urwid.ExitMainLoop() 41 | # Right 42 | elif ch == self.conf.keys['right'] or ch == 'right': 43 | self.interface.navigate_buffer(+1) 44 | # left 45 | elif ch == self.conf.keys['left'] or ch == 'left': 46 | self.interface.navigate_buffer(-1) 47 | elif ch == self.conf.keys['up']: 48 | self.interface.go_up() 49 | elif ch == self.conf.keys['down']: 50 | self.interface.go_down() 51 | # Update 52 | elif ch == self.conf.keys['update']: 53 | self.api.update_timeline(self.interface.buffer) 54 | # Tweet 55 | elif ch == self.conf.keys['tweet']: 56 | self.interface.edit_status('tweet', prompt='Tweet ') 57 | # Reply 58 | elif ch == self.conf.keys['reply']: 59 | self.interface.reply() 60 | # Retweet 61 | elif ch == self.conf.keys['retweet']: 62 | self.api.retweet() 63 | # Retweet and Edit 64 | elif ch == self.conf.keys['retweet_and_edit']: 65 | self.api.retweet_and_edit() 66 | # Delete 67 | elif ch == self.conf.keys['delete']: 68 | self.api.destroy() 69 | # Mention timeline 70 | elif ch == self.conf.keys['mentions']: 71 | self.interface.change_buffer('mentions') 72 | # Home Timeline 73 | elif ch == self.conf.keys['home']: 74 | self.interface.change_buffer('home') 75 | # Direct Message Timeline 76 | elif ch == self.conf.keys['getDM']: 77 | self.interface.change_buffer('direct') 78 | # Clear statuses 79 | elif ch == self.conf.keys['clear']: 80 | self.interface.clear_statuses() 81 | # Follow Selected 82 | elif ch == self.conf.keys['follow_selected']: 83 | self.api.follow_selected() 84 | # Unfollow Selected 85 | elif ch == self.conf.keys['unfollow_selected']: 86 | self.api.unfollow_selected() 87 | # Follow 88 | elif ch == self.conf.keys['follow']: 89 | self.interface.edit_status('follow', prompt='Follow') 90 | # Unfollow 91 | elif ch == self.conf.keys['unfollow']: 92 | self.interface.edit_status('unfollow', prompt='Unfollow ') 93 | # Open URL 94 | elif ch == self.conf.keys['openurl']: 95 | self.interface.openurl() 96 | # Search 97 | elif ch == self.conf.keys['search']: 98 | self.interface.edit_status('search', prompt='Search ') 99 | # Search User 100 | elif ch == self.conf.keys['search_user']: 101 | self.interface.edit_status('public', prompt='Nick ') 102 | # Search Myself 103 | elif ch == self.conf.keys['search_myself']: 104 | self.api.my_public_timeline() 105 | # Search Current User 106 | elif ch == self.conf.keys['search_current_user']: 107 | self.api.find_current_public_timeline() 108 | # Send Direct Message 109 | #FIXME 110 | #elif ch == self.conf.keys['sendDM']: 111 | #self.api.direct_message() 112 | # Create favorite 113 | elif ch == self.conf.keys['fav']: 114 | self.api.set_favorite() 115 | # Get favorite 116 | elif ch == self.conf.keys['get_fav']: 117 | self.api.get_favorites() 118 | # Destroy favorite 119 | elif ch == self.conf.keys['delete_fav']: 120 | self.api.destroy_favorite() 121 | # Thread 122 | elif ch == self.conf.keys['thread']: 123 | self.api.get_thread() 124 | # Open image 125 | elif ch == self.conf.keys['open_image']: 126 | open_image(self.interface.current_status().user) 127 | # User info 128 | elif ch == 'i': 129 | self.interface.current_user_info() 130 | # Waterline 131 | elif ch == self.conf.keys['waterline']: 132 | self.interface.update_last_read_home() 133 | # Back on Top 134 | elif ch == self.conf.keys['back_on_top']: 135 | self.interface.back_on_top() 136 | # Back on Bottom 137 | elif ch == self.conf.keys['back_on_bottom']: 138 | self.interface.back_on_bottom() 139 | # Get list 140 | elif ch == self.conf.keys['do_list']: 141 | self.interface.edit_status('list', prompt='List ') 142 | # Help 143 | elif ch == '?': 144 | self.interface.display_help() 145 | 146 | self.interface.display_timeline() 147 | 148 | else: 149 | if ch in ('q', 'Q', 'esc'): 150 | urwid.emit_signal(self, 'help_done') 151 | 152 | -------------------------------------------------------------------------------- /doc/changelog: -------------------------------------------------------------------------------- 1 | Date Format : YYYY-MM-DD 2 | 3 | * Version 0.6.2 -- 2011-12-23 4 | 5 | * Auto-Completion nicks when tweeting 6 | * utf8decode bugs fixes for urwid > 1.0.0 7 | * Version arguments with -v/--version 8 | * Validation for send a tweet need "enter" twice 9 | * Generating configuration fixed #110 10 | * Clear statuses fixed 'c' 11 | 12 | * Version 0.6.1 -- 2011-12-18 13 | 14 | * 'j' and 'k' fixed #99 15 | * typos 16 | * configuration for colors are back 17 | * the line from status box has it own color attribute 18 | * follow/unwollow was the same command #104 19 | * cleanup 20 | 21 | * Version 0.6.0 -- 2011-11-30 22 | 23 | * First release with urwid support 24 | * Much better editing tweets 25 | * Better utf-8 support 26 | * UI entirely redone 27 | 28 | * Version 0.5.0 -- 2011-11-06 29 | 30 | * Better update thread, no more freeze after a while 31 | * Proxy support 32 | * update waterline with 'w' to display a line for the last read 33 | * Bugs fixed 34 | 35 | * Version 0.4.2 -- 2011-09-20 36 | 37 | * Retweet counter 38 | * Relative time now update correctly 39 | * Few default values changed 40 | * Better display with waterline 41 | * Force DM reply if you are in DM timeline 42 | * Compress param rename to compact 43 | * New header template in params 44 | 45 | * Version 0.4.1 -- 2011-07-27 46 | 47 | * Goo.gl shortener service added. 48 | * Musd.pl shortener service added. (Hobbestigrou contribution) 49 | * Logging system added, with customizable verbosity, stored in 50 | ~/.config/tyrs/tyrs.log 51 | * New curstomer_key and customer_secret params for own StatusNet instance. 52 | * Doc and help screen updated 53 | * Draw a line to mark your last tweets read in previous session 54 | * Updating issues when networks down fixed #83 55 | 56 | * Version 0.4.0 -- 2011-06-30 57 | 58 | * Url shortening service enable, with ur1.ca and bit.ly 59 | * Date appends to tweets when older than the current day 60 | * Lowercase enable in config for control-sequence - thanks quite 61 | * Default value for refresh set to 3 minutes 62 | * New filter system, allow to filter tweets without urls, and lots 63 | customisazion on it 64 | * Input box moves to the bottom 65 | * Open user avatar with feh (customizable) 66 | * lazzy loading of tweets when scroll down, cycle to infinyte 67 | * Lots of documentation update and review, thank jasonwryan 68 | 69 | * Version 0.3.3 -- 2011-06-19 70 | 71 | * Changes for pypi site 72 | * Better resize event handle 73 | * Default config file generate if there is none 74 | * Own retweet timeline 75 | * Thread function and timeline 76 | * utf symbols add 77 | * bugs fixes 78 | 79 | * Version 0.3.2 -- 2011-06-15 80 | 81 | * Encoding issues with translation 82 | * Wrong data with mentions tweets 83 | 84 | * Version 0.3.1 -- 2011-06-14 85 | 86 | * Almost all code changes, review, improved, moved... 87 | * Internationalisation with french and spanish translation (thanks to 88 | diegoxter) 89 | * Better display customisation with a padding/margin system 90 | * Bugs fixed, imports improves 91 | 92 | * Version 0.3.0 -- 2011-06-05 93 | 94 | * Identi.ca support 95 | * Favorites create, destroy and timeline 96 | * Bugs on empty DM timeline fixed - issue #33 97 | * Man page added, with 'man tyrs' - thanks jasonwryan 98 | * Setup.py review for better dependencies managment - issue #1 99 | * Better display of tweets, the width should always correspond to the tweet now 100 | * Few bugs fixed 101 | 102 | * Version 0.2.4 -- 2011-05-31 103 | 104 | * Help screen with '?' 105 | * Help bar on bottom desactivable with params: help=0 106 | * Navigation between timelines improves 107 | * User timeline, with a shortcut for self public timeline 108 | * Destroy tweets with 'C' 109 | * Compress params, allow more visible tweets on screen 110 | * Better display tweets - issue #29 111 | * Redraw screen with '^L' 112 | * Lots of bugs fixed 113 | 114 | * Version 0.2.3 -- 2011-05-28 115 | 116 | * Environment variable read from $BROWSER, issue #23 117 | * Warning for invalide arguments on bold list, issue #19 118 | * Environment variable read from $XDG_CONFIG_HOME 119 | * Config file generator from command line, with --generate-config filename, or -g filename 120 | * Colors in activities bar, with current tab, and unread highlight color 121 | * Navigation throught timelines enable with left/right arrow or 'J', 'K' 122 | * Documentation and website corrected. Cheers @jasonwryan 123 | * Lots code refactored 124 | 125 | * Version 0.2.2 -- 2011-05-25 126 | 127 | * Managment of differents timelimes review 128 | * Empty spaces bug in tweets fixed 129 | * Control keys enable in configuration file - issue #18 130 | * Bold colors enable and easely configurable 131 | * Current tweet color added 132 | * Buffer activities conter added (need improvement) 133 | * Refactoring codes 134 | 135 | * Version 0.2.1 -- 2011-05-23 136 | 137 | * Direct messages in/out 'd', 'D' - issue #7 138 | * Mentions tweets timeline 'm' - issue #12 139 | * Reply to a tweet 'M' 140 | * Go back to bottom, top 'g', 'G' - issue #14 141 | * Search 's' 142 | 143 | * Version 0.2.0 -- 2011-05-21 144 | 145 | * Transparency support added 146 | * Unicode support improved 147 | * 256 Colors configuration aviable 148 | * Retweet can be editable with 'R' - issue #3 149 | * New online documentation and site aviable http://tyrs.nicosphere.net/ 150 | * Timeline navigation review 151 | 152 | * Version 0.1.4.1 to 0.1.4.3-- 2011-05-18 153 | 154 | * Bugs fixed 155 | 156 | * Version 0.1.4 -- 2011-05-18 157 | 158 | * All documentation review 159 | * Stay on current tweet when updated timeline (issue #5) 160 | * Open url of the current tweet with firefox when stock 'o' (issue #4) 161 | 162 | * Version 0.1.3 -- 2011-05-15 163 | 164 | * Follow and unfollow ('f', 'F', 'l', 'L') 165 | * Desappearing tweet box bug fixed - issue #2 166 | * Commande line for account and config (changing the account directly with the CLI) 167 | * Refactoring code 168 | 169 | * Version 0.1.2 -- 2011-05-13 170 | 171 | * Retweet when strock 'r' touch 172 | * Character counter add when tweeting 173 | * Clear statuses when stock 'c' 174 | * Update timeline with 'u' 175 | * Relative time enable with config ('xx hours ago') 176 | * Localtime bug fixed 177 | * original name and retweeter name display for retweet 178 | * Better looking for retweets 179 | * Improving messages information 180 | * More configuration aviable 181 | 182 | * Version 0.1.1 -- 2011-05-11 183 | 184 | * Resize window handle. 185 | * Warning message if the network's down 186 | * No more bugs when network's down 187 | * Refresh properly the home timeline after sending a tweet 188 | * Configuration color for warning message 189 | * New tweets now keept as full history 190 | * Refactoring 191 | 192 | * Version 0.1.0 -- 2011-05-09 193 | 194 | * UTF-8 support 195 | * Oauth authentification support 196 | * Display tweets with some basics colors 197 | * Configuration file very basic 198 | * Send a tweet 199 | * Documentation for quick start 200 | -------------------------------------------------------------------------------- /src/tyrs/interface.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import re 17 | import os 18 | import tyrs 19 | import urwid 20 | import curses 21 | import logging 22 | from user import User 23 | from keys import Keys 24 | from help import help_bar, Help 25 | from utils import get_urls 26 | from constant import palette 27 | from editor import TweetEditor 28 | from update import UpdateThread 29 | from widget import HeaderWidget 30 | from completion import Completion 31 | import urwid.html_fragment 32 | 33 | class Interface(object): 34 | 35 | def __init__(self): 36 | self.api = tyrs.container['api'] 37 | self.conf = tyrs.container['conf'] 38 | self.timelines = tyrs.container['timelines'] 39 | self.buffers = tyrs.container['buffers'] 40 | self.completion = tyrs.container['completion'] 41 | self.help = False 42 | tyrs.container.add('interface', self) 43 | self.update_last_read_home() 44 | self.api.set_interface() 45 | self.regex_retweet = re.compile('^RT @\w+:') 46 | self.stoped = False 47 | self.buffer = 'home' 48 | self.first_update() 49 | self.main_loop() 50 | 51 | def main_loop (self): 52 | 53 | self.header = HeaderWidget() 54 | foot = help_bar() 55 | self.listbox = self.select_current_timeline().timeline 56 | self.main_frame = urwid.Frame(urwid.AttrWrap(self.listbox, 'body'), header=self.header, footer=foot) 57 | key_handle = Keys() 58 | urwid.connect_signal(key_handle, 'help_done', self.help_done) 59 | self.loop = urwid.MainLoop(self.main_frame, palette, unhandled_input=key_handle.keystroke) 60 | update = UpdateThread() 61 | update.start() 62 | self.loop.run() 63 | update._Thread__stop() 64 | update.stop() 65 | 66 | def reply(self): 67 | self.status = self.current_status() 68 | if hasattr(self.status, 'user'): 69 | nick = self.status.user.screen_name 70 | #FIXME: 71 | #else: 72 | #self.direct_message() 73 | data = '@' + nick 74 | self.edit_status('reply', data, 'Tweet ') 75 | 76 | def edit_status(self, action, content='', prompt=''): 77 | self.foot = TweetEditor(content, prompt) 78 | self.main_frame.set_footer(self.foot) 79 | self.main_frame.set_focus('footer') 80 | if action == 'tweet': 81 | urwid.connect_signal(self.foot, 'done', self.api.tweet_done) 82 | elif action == 'reply': 83 | urwid.connect_signal(self.foot, 'done', self.api.reply_done) 84 | elif action == 'follow': 85 | urwid.connect_signal(self.foot, 'done', self.api.follow_done) 86 | elif action == 'unfollow': 87 | urwid.connect_signal(self.foot, 'done', self.api.unfollow_done) 88 | elif action == 'search': 89 | urwid.connect_signal(self.foot, 'done', self.api.search_done) 90 | elif action == 'public': 91 | urwid.connect_signal(self.foot, 'done', self.api.public_done) 92 | elif action == 'list': 93 | urwid.connect_signal(self.foot, 'done', self.api.list_done) 94 | 95 | def first_update(self): 96 | updates = ['user_retweet', 'favorite'] 97 | for buff in updates: 98 | self.api.update_timeline(buff) 99 | self.timelines[buff].reset() 100 | self.timelines[buff].all_read() 101 | 102 | def display_timeline (self): 103 | if not self.help: 104 | timeline = self.select_current_timeline() 105 | self.listbox = timeline.timeline 106 | self.main_frame.set_body(urwid.AttrWrap(self.listbox, 'body')) 107 | if self.buffer == 'home': 108 | self.conf.save_last_read(timeline.last_read) 109 | self.display_flash_message() 110 | 111 | def lazzy_load(self): 112 | timeline = self.select_current_timeline() 113 | focus = timeline.timeline.get_focus()[1] 114 | if timeline.cleared != False: 115 | return 116 | if focus is len(timeline.walker)-1: 117 | timeline.page += 1 118 | statuses = self.api.retreive_statuses(self.buffer, timeline.page) 119 | timeline.append_old_statuses(statuses) 120 | self.display_timeline() 121 | 122 | def redraw_screen (self): 123 | self.loop.draw_screen() 124 | 125 | def display_flash_message(self): 126 | if hasattr(self, 'main_frame'): 127 | header = HeaderWidget() 128 | self.main_frame.set_header(header) 129 | self.redraw_screen() 130 | self.api.flash_message.reset() 131 | 132 | def erase_flash_message(self): 133 | self.api.flash_message.reset() 134 | self.display_flash_message() 135 | 136 | def change_buffer(self, buffer): 137 | self.buffer = buffer 138 | self.timelines[buffer].reset() 139 | 140 | def navigate_buffer(self, nav): 141 | '''Navigate with the arrow, mean nav should be -1 or +1''' 142 | index = self.buffers.index(self.buffer) 143 | new_index = index + nav 144 | if new_index >= 0 and new_index < len(self.buffers): 145 | self.change_buffer(self.buffers[new_index]) 146 | 147 | def check_for_last_read(self, id): 148 | if self.last_read_home == str(id): 149 | return True 150 | return False 151 | 152 | def select_current_timeline(self): 153 | return self.timelines[self.buffer] 154 | 155 | def clear_statuses(self): 156 | timeline = self.select_current_timeline() 157 | timeline.count_statuses() 158 | timeline.reset() 159 | timeline.clear() 160 | 161 | def current_status(self): 162 | focus = self.listbox.get_focus()[0] 163 | return focus.status 164 | 165 | def display_help(self): 166 | self.help = True 167 | h = Help() 168 | self.main_frame.set_body(h) 169 | 170 | def help_done(self): 171 | self.help = False 172 | self.display_timeline() 173 | 174 | def back_on_bottom(self): 175 | timeline = self.select_current_timeline() 176 | self.listbox.set_focus(timeline.status_count()) 177 | 178 | def back_on_top(self): 179 | self.listbox.set_focus(0) 180 | 181 | def openurl(self): 182 | urls = get_urls(self.current_status().text) 183 | for url in urls: 184 | try: 185 | os.system(self.conf.params['openurl_command'] % url + '> /dev/null 2>&1') 186 | except: 187 | logging.error('openurl error') 188 | 189 | def update_last_read_home(self): 190 | self.last_read_home = self.conf.load_last_read() 191 | 192 | def current_user_info(self): 193 | User(self.current_status().user) 194 | 195 | def go_up(self): 196 | timeline = self.select_current_timeline() 197 | timeline.go_up() 198 | 199 | def go_down(self): 200 | timeline = self.select_current_timeline() 201 | timeline.go_down() 202 | 203 | def beep(self): 204 | return curses.beep() 205 | -------------------------------------------------------------------------------- /po/es.po: -------------------------------------------------------------------------------- 1 | # Internationalisation for Tyrs. 2 | # Copyright (C) 2011 3 | # This file is distributed under the same license as the Tyrs package. 4 | # Nicolas Paris 2011 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: 0.3.1\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2011-06-15 18:30-0430\n" 11 | "PO-Revision-Date: 2011-06-12 10:13-0430\n" 12 | "Last-Translator: Diego Jose \n" 13 | "Language-Team: Spanish\n" 14 | "Language: es\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #: src/message.py:22 21 | msgid "Updating timeline..." 22 | msgstr "Actualizando timeline..." 23 | 24 | #: src/message.py:23 25 | msgid "Couldn't retrieve tweets" 26 | msgstr "No se recibirieron los tweets" 27 | 28 | #: src/message.py:26 29 | msgid "Your tweet was sent" 30 | msgstr "Tu tweet fue enviado" 31 | 32 | #: src/message.py:27 33 | msgid "Couldn't send tweet" 34 | msgstr "No se pudo enviar el tweet" 35 | 36 | #: src/message.py:30 37 | msgid "Your retweet was sent" 38 | msgstr "Tu retweet fue enviado" 39 | 40 | #: src/message.py:31 41 | msgid "Couldn't send retweet" 42 | msgstr "No se pudo enviar el retweet" 43 | 44 | #: src/message.py:34 45 | msgid "You have deleted the tweet" 46 | msgstr "Borraste el tweet" 47 | 48 | #: src/message.py:35 49 | msgid "Couldn't delete tweet" 50 | msgstr "No se pudo borrar el tweet" 51 | 52 | #: src/message.py:38 53 | msgid "The tweet was added to favorites list" 54 | msgstr "El tweet fue agregado a tus favoritos" 55 | 56 | #: src/message.py:39 57 | msgid "Couldn't add tweet to favorites list" 58 | msgstr "No se pudo agregar el tweet a tus favoritos" 59 | 60 | #: src/message.py:42 61 | msgid "Tweet was removed from favorites list" 62 | msgstr "El tweet fue removido de tus favoritos" 63 | 64 | #: src/message.py:43 65 | msgid "Couldn't delete tweet on favorites list" 66 | msgstr "No se pudo quitar el tweet de tus favoritos" 67 | 68 | #: src/message.py:46 69 | msgid "Direct message was sent" 70 | msgstr "El mensaje directo fue enviado" 71 | 72 | #: src/message.py:47 73 | msgid "Couldn't send direct message" 74 | msgstr "No se pudo enviar el mensaje directo" 75 | 76 | #: src/message.py:50 77 | #, python-format 78 | msgid "You are now following %s" 79 | msgstr "Ahora sigues a %s" 80 | 81 | #: src/message.py:51 82 | #, python-format 83 | msgid "Couldn't follow %s" 84 | msgstr "No se ha podido seguir a %s" 85 | 86 | #: src/message.py:54 87 | #, python-format 88 | msgid "You are not following %s anymore" 89 | msgstr "Ya no sigues a %s" 90 | 91 | #: src/message.py:55 92 | #, python-format 93 | msgid "Couldn't stop following %s" 94 | msgstr "No se pudo dar unfollow a %s" 95 | 96 | #: src/message.py:58 97 | #, python-format 98 | msgid "Search results for %s" 99 | msgstr "Resultados de la búsqueda '%s'" 100 | 101 | #: src/message.py:59 102 | #, python-format 103 | msgid "Couldn't search for %s" 104 | msgstr "No se pudo buscar '%s'" 105 | 106 | #: src/message.py:86 107 | msgid "Couldn't find any profile." 108 | msgstr "No se detectó ningún perfil" 109 | 110 | #: src/message.py:88 111 | #, python-format 112 | msgid "It should reside in: %s" 113 | msgstr "Debería estar en %s" 114 | 115 | #: src/message.py:89 116 | msgid "If you want to setup a new account, then follow these steps" 117 | msgstr "Si quieres configurar una cuenta nueva, comenzemos configurando lo básico" 118 | 119 | #: src/message.py:90 120 | msgid "If you want to skip this, just press return or ctrl-C." 121 | msgstr "Si no quieres hacer esto, presiona return o ctrl-C." 122 | 123 | #: src/message.py:94 124 | msgid "Which service do you want to use?" 125 | msgstr "¿Cual servicio deseas usar? " 126 | 127 | #: src/message.py:103 128 | msgid "" 129 | "Which root url do you want? (leave blank for default, https://identi.ca/api)" 130 | msgstr "" 131 | "¿Cuál url quieres usar? (deja en blanco para usar el valor por defecto " 132 | "https://identi.identi.ca/api)" 133 | 134 | 135 | #: src/config.py:125 tyrs/config.py:137 136 | msgid "Your choice? > " 137 | msgstr "¿Tu elección?" 138 | 139 | #: src/config.py:249 140 | #, python-format 141 | msgid "The param \"%s\" does not exist for bold colors" 142 | msgstr "El parámetro \"%s\" no se permite en negrita" 143 | 144 | #: src/config.py:286 145 | msgid "Requesting temp token from " 146 | msgstr "Obteniendo una permiso temporal desde " 147 | 148 | #: src/config.py:291 149 | msgid "Invalid respond from " 150 | msgstr "Respuesta inválida desde " 151 | 152 | #: src/config.py:292 153 | #, python-format 154 | msgid " requesting temp token: %s" 155 | msgstr " obteniendo permiso temporal: %s" 156 | 157 | #: src/config.py:297 158 | msgid "Please visit the following page to retrieve pin code needed" 159 | msgstr "Por favor, visita esta página para obtener el código necesario" 160 | 161 | #: src/config.py:298 162 | msgid "to obtain an Authentication Token:" 163 | msgstr "para obtener el Permiso de Autentificación:" 164 | 165 | #: src/config.py:309 166 | msgid "Generating and signing request for an access token" 167 | msgstr "Generando y firmando el pedido para un permiso de acceso" 168 | 169 | #: src/config.py:317 170 | #, python-format 171 | msgid "Request for access token failed: %s" 172 | msgstr "El pedido falló: %s" 173 | 174 | #: src/config.py:330 175 | msgid "Error creating directory .config/tyrs" 176 | msgstr "Error al crear el directorio .config/tyrs" 177 | 178 | #: src/config.py:342 179 | msgid "your account has been saved" 180 | msgstr "tu cuenta ha sido guardada" 181 | 182 | #: src/editor.py:139 183 | msgid "What's up?" 184 | msgstr "¿Qué está pasando?" 185 | 186 | #: src/editor.py:142 187 | msgid "Entry a name" 188 | msgstr "Inserta un nombre" 189 | 190 | #: src/editor.py:145 191 | msgid "Search for something?" 192 | msgstr "¿Qué quieres buscar?" 193 | 194 | #: src/help.py:36 195 | msgid "Navigation" 196 | msgstr "Navegación" 197 | 198 | #: src/help.py:37 199 | msgid "Go up one tweet" 200 | msgstr "Moverte hacia arriba" 201 | 202 | #: src/help.py:38 203 | msgid "Go down one tweet" 204 | msgstr "Moverte hacia abajo" 205 | 206 | #: src/help.py:39 207 | msgid "Go to top of screen" 208 | msgstr "Ir hacia el tope" 209 | 210 | #: src/help.py:40 211 | msgid "Go to bottom of screen" 212 | msgstr "Ir hacia el fondo de la pantalla" 213 | 214 | #: src/help.py:42 215 | msgid "Timelines" 216 | msgstr "Timelines" 217 | 218 | #: src/help.py:43 219 | msgid "Go left on the timeline's bar" 220 | msgstr "Moverse hacia la izquierda entre timelines" 221 | 222 | #: src/help.py:44 223 | msgid "Go right on the timeline's bar" 224 | msgstr "Moverse hacia la derecha entre timelines" 225 | 226 | #: src/help.py:45 227 | msgid "Refresh current timeline" 228 | msgstr "Refrescar el timeline actual" 229 | 230 | #: src/help.py:46 231 | msgid "Clear all but last tweet in timeline" 232 | msgstr "Limpiar el timeline excepto el último tweet" 233 | 234 | #: src/help.py:47 235 | msgid "Go to home timeline" 236 | msgstr "Moverse hacia el timeline de inicio" 237 | 238 | #: src/help.py:48 239 | msgid "Go to mentions timeline" 240 | msgstr "Ver las menciones" 241 | 242 | #: src/help.py:49 243 | msgid "Go to direct message timeline" 244 | msgstr "Ver los mensajes directos" 245 | 246 | #: src/help.py:50 247 | msgid "Search for term and show resulting timeline" 248 | msgstr "Buscar un término o un username" 249 | 250 | #: src/help.py:51 251 | msgid "Show somebody's public timeline" 252 | msgstr "Revisar el timeline de alguien" 253 | 254 | #: src/help.py:52 255 | msgid "Show your public timeline" 256 | msgstr "Revisa tu propio timeline" 257 | 258 | #: src/help.py:54 259 | msgid "Tweets" 260 | msgstr "Tweets" 261 | 262 | #: src/help.py:55 263 | msgid "Send a tweet" 264 | msgstr "Enviar un tweet" 265 | 266 | #: src/help.py:56 267 | msgid "Retweet selected tweet" 268 | msgstr "Hacer retweet al tweet seleccionado" 269 | 270 | #: src/help.py:57 271 | msgid "Retweet selected tweet, but edit first" 272 | msgstr "Hacer retweet manual al tweet seleccionado" 273 | 274 | #: src/help.py:58 275 | msgid "Reply to selected tweet" 276 | msgstr "Responder al tweet seleccionado" 277 | 278 | #: src/help.py:59 279 | msgid "Send direct message" 280 | msgstr "Envia un mensaje directo" 281 | 282 | #: src/help.py:60 283 | msgid "Delete selected tweet (must be yours)" 284 | msgstr "Borrar el tweet seleccionado (debe ser tuyo)" 285 | 286 | #: src/help.py:63 287 | msgid "Follow selected twitter" 288 | msgstr "Seguir al twittero seleccionado" 289 | 290 | #: src/help.py:64 291 | msgid "Unfollow selected twitter" 292 | msgstr "Dejar de seguir al twittero seleccionado" 293 | 294 | #: src/help.py:65 295 | msgid "Follow a twitter" 296 | msgstr "Seguir a un twittero" 297 | 298 | #: src/help.py:66 299 | msgid "Unfollow a twitter" 300 | msgstr "Dejar de seguir a un twittero" 301 | 302 | #: src/help.py:68 303 | msgid "Others" 304 | msgstr "Otros" 305 | 306 | #: src/help.py:69 307 | msgid "Leave Tyrs" 308 | msgstr "Salir de Tyrs" 309 | 310 | #: src/help.py:70 311 | msgid "Open URL in browser" 312 | msgstr "Abrir url en el navegador" 313 | 314 | #: src/help.py:71 315 | msgid "Redraw the screen" 316 | msgstr "Redibujar la pantalla" 317 | 318 | #: src/help.py:91 319 | msgid "Name" 320 | msgstr "Nombre" 321 | 322 | #: src/help.py:92 323 | msgid "Key" 324 | msgstr "Tecla" 325 | 326 | #: src/help.py:94 327 | msgid "Description" 328 | msgstr "Descripción" 329 | -------------------------------------------------------------------------------- /po/fr.po: -------------------------------------------------------------------------------- 1 | # Tyrs 2 | # Copyright (C) 2011 Nicolas Paris 3 | # This file is distributed under the same license as the Tyrs package. 4 | # Nicolas Paris 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: 0.3.1\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2011-06-16 00:33+0200\n" 11 | "PO-Revision-Date: 2011-06-12 01:55+0200\n" 12 | "Last-Translator: \n" 13 | "Language-Team: French\n" 14 | "Language: fr\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 19 | 20 | #: src/message.py:22 21 | msgid "Updating timeline..." 22 | msgstr "Mise à jour de timeline..." 23 | 24 | #: src/message.py:23 25 | #, fuzzy 26 | msgid "Couldn't retrieve tweets" 27 | msgstr "Impossible de recevoir les tweets" 28 | 29 | #: src/message.py:26 30 | #, fuzzy 31 | msgid "Your tweet was sent" 32 | msgstr "Votre tweet à été envoyé" 33 | 34 | #: src/message.py:27 35 | #, fuzzy 36 | msgid "Couldn't send tweet" 37 | msgstr "Impossible d'envoyer le tweet" 38 | 39 | #: src/message.py:30 40 | #, fuzzy 41 | msgid "Your retweet was sent" 42 | msgstr "Votre retweet à été envoyé" 43 | 44 | #: src/message.py:31 45 | #, fuzzy 46 | msgid "Couldn't send retweet" 47 | msgstr "Impossible d'envoyer le retweet" 48 | 49 | #: src/message.py:34 50 | #, fuzzy 51 | msgid "You have deleted the tweet" 52 | msgstr "Vous avez effacer le tweet" 53 | 54 | #: src/message.py:35 55 | #, fuzzy 56 | msgid "Couldn't delete tweet" 57 | msgstr "Impossible d'envoyer le tweet" 58 | 59 | #: src/message.py:38 60 | #, fuzzy 61 | msgid "The tweet was added to favorites list" 62 | msgstr "Le tweet est maintenant dans vos favories" 63 | 64 | #: src/message.py:39 65 | #, fuzzy 66 | msgid "Couldn't add tweet to favorites list" 67 | msgstr "Impossible d'ajouter le tweet dans vos favories" 68 | 69 | #: src/message.py:42 70 | #, fuzzy 71 | msgid "Tweet was removed from favorites list" 72 | msgstr "Le tweet est effacé de vos favories" 73 | 74 | #: src/message.py:43 75 | #, fuzzy 76 | msgid "Couldn't delete tweet on favorites list" 77 | msgstr "Impossible d'effacer le tweet dans vos favories" 78 | 79 | #: src/message.py:46 80 | #, fuzzy 81 | msgid "Direct message was sent" 82 | msgstr "Votre message direct à été envoyé" 83 | 84 | #: src/message.py:47 85 | #, fuzzy 86 | msgid "Couldn't send direct message" 87 | msgstr "Impossible d'envoyer le message direct" 88 | 89 | #: src/message.py:50 90 | #, python-format 91 | msgid "You are now following %s" 92 | msgstr "Vous suivez maintenant %s" 93 | 94 | #: src/message.py:51 95 | #, fuzzy, python-format 96 | msgid "Couldn't follow %s" 97 | msgstr "Impossible de suivre %s" 98 | 99 | #: src/message.py:54 100 | #, fuzzy, python-format 101 | msgid "You are not following %s anymore" 102 | msgstr "Vous ne suivez plus %s" 103 | 104 | #: src/message.py:55 105 | #, fuzzy, python-format 106 | msgid "Couldn't stop following %s" 107 | msgstr "Impossible de ne plus suivre %s" 108 | 109 | #: src/message.py:58 110 | #, python-format 111 | msgid "Search results for %s" 112 | msgstr "Résultat de recherche pour" 113 | 114 | #: src/message.py:59 115 | #, fuzzy, python-format 116 | msgid "Couldn't search for %s" 117 | msgstr "Recherche impossible pour %s" 118 | 119 | #: src/message.py:86 120 | msgid "Couldn't find any profile." 121 | msgstr "Impossible de trouver la personne" 122 | 123 | #: src/message.py:88 124 | #, fuzzy, python-format 125 | msgid "It should reside in: %s" 126 | msgstr "Il devrait être dans %s" 127 | 128 | #: src/message.py:89 129 | #, fuzzy 130 | msgid "If you want to setup a new account, then follow these steps" 131 | msgstr "Si vous voulez paramétrer un nouveau compte, suivez ces quelques procédures" 132 | 133 | #: src/message.py:90 134 | msgid "If you want to skip this, just press return or ctrl-C." 135 | msgstr "Si vous voulez passer cela, presser simplement retour ou ctrl-C" 136 | 137 | #: src/message.py:94 138 | msgid "Which service do you want to use?" 139 | msgstr "Quel service voulez vous utiliser ?" 140 | 141 | #: src/message.py:103 142 | #, fuzzy 143 | msgid "" 144 | "Which root url do you want? (leave blank for default, https://identi.ca/api)" 145 | msgstr "" 146 | "Quel url racine voulez vous utiliser ? (laisser blanc pour la valeur par " 147 | "défaut, https://identi.ca/api)" 148 | 149 | #: src/config.py:125 tyrs/config.py:137 150 | msgid "Your choice? > " 151 | msgstr "Votre choix ? >" 152 | 153 | #: src/config.py:249 154 | #, python-format 155 | msgid "The param \"%s\" does not exist for bold colors" 156 | msgstr "Le paramètre \"%s\" n'existe pas pour les couleurs" 157 | 158 | #: src/config.py:286 159 | msgid "Requesting temp token from " 160 | msgstr "Demande de jeton temporaire depuis " 161 | 162 | #: src/config.py:291 163 | msgid "Invalid respond from " 164 | msgstr "Réponse invalide de " 165 | 166 | #: src/config.py:292 167 | #, python-format 168 | msgid " requesting temp token: %s" 169 | msgstr " demande de jeton temporaire: %s" 170 | 171 | #: src/config.py:297 172 | #, fuzzy 173 | msgid "Please visit the following page to retrieve pin code needed" 174 | msgstr "Visitez s'il vous plaît la page, et entrez le code pin en retour" 175 | 176 | #: src/config.py:298 177 | #, fuzzy 178 | msgid "to obtain an Authentication Token:" 179 | msgstr "pour obtenir le jeton d'autentification :" 180 | 181 | #: src/config.py:309 182 | msgid "Generating and signing request for an access token" 183 | msgstr "Génération et signature de la demande pour un jeton d'accès" 184 | 185 | #: src/config.py:317 186 | #, fuzzy, python-format 187 | msgid "Request for access token failed: %s" 188 | msgstr "La demande de jeton n'a pas aboutis: %s" 189 | 190 | #: src/config.py:330 191 | #, fuzzy 192 | msgid "Error creating directory .config/tyrs" 193 | msgstr "Erreur lors de la création de répertoire .config/tyrs" 194 | 195 | #: src/config.py:342 196 | msgid "your account has been saved" 197 | msgstr "Votre compte à été sauvegardé" 198 | 199 | #: src/editor.py:139 200 | msgid "What's up?" 201 | msgstr "Quoi de neuf ?" 202 | 203 | #: src/editor.py:142 204 | msgid "Entry a name" 205 | msgstr "Entrez un nom" 206 | 207 | #: src/editor.py:145 208 | msgid "Search for something?" 209 | msgstr "Cherchez quelque chose ?" 210 | 211 | #: src/help.py:36 212 | msgid "Navigation" 213 | msgstr "Navigation" 214 | 215 | #: src/help.py:37 216 | #, fuzzy 217 | msgid "Go up one tweet" 218 | msgstr "Déplacement d'un tweet vers le haut" 219 | 220 | #: src/help.py:38 221 | msgid "Go down one tweet" 222 | msgstr "Déplacement d'un tweet vers le bas" 223 | 224 | #: src/help.py:39 225 | #, fuzzy 226 | msgid "Go to top of screen" 227 | msgstr "Déplacement vers le haut de l'écran" 228 | 229 | #: src/help.py:40 230 | #, fuzzy 231 | msgid "Go to bottom of screen" 232 | msgstr "Déplacement vers le bas de l'écran" 233 | 234 | #: src/help.py:42 235 | msgid "Timelines" 236 | msgstr "Timelines" 237 | 238 | #: src/help.py:43 239 | #, fuzzy 240 | msgid "Go left on the timeline's bar" 241 | msgstr "Déplacement à gauche dans les timelines" 242 | 243 | #: src/help.py:44 244 | #, fuzzy 245 | msgid "Go right on the timeline's bar" 246 | msgstr "Déplacement à droite dans les timelines" 247 | 248 | #: src/help.py:45 249 | #, fuzzy 250 | msgid "Refresh current timeline" 251 | msgstr "Rafraîchir la timeline en cours" 252 | 253 | #: src/help.py:46 254 | #, fuzzy 255 | msgid "Clear all but last tweet in timeline" 256 | msgstr "Effacer, et ne laisser que le dernier tweet dans votre timeline" 257 | 258 | #: src/help.py:47 259 | #, fuzzy 260 | msgid "Go to home timeline" 261 | msgstr "Déplacement vers la timeline home" 262 | 263 | #: src/help.py:48 264 | #, fuzzy 265 | msgid "Go to mentions timeline" 266 | msgstr "Déplacement vers la timeline des mentions" 267 | 268 | #: src/help.py:49 269 | #, fuzzy 270 | msgid "Go to direct message timeline" 271 | msgstr "Déplacement vers la timeline des messages directs" 272 | 273 | #: src/help.py:50 274 | msgid "Search for term and show resulting timeline" 275 | msgstr "Recherche pour un terme donné" 276 | 277 | #: src/help.py:51 278 | #, fuzzy 279 | msgid "Show somebody's public timeline" 280 | msgstr "Charger la timeline publique de quelqu'un" 281 | 282 | #: src/help.py:52 283 | #, fuzzy 284 | msgid "Show your public timeline" 285 | msgstr "Charger votre propre timeline publique" 286 | 287 | #: src/help.py:54 288 | msgid "Tweets" 289 | msgstr "Tweets" 290 | 291 | #: src/help.py:55 292 | msgid "Send a tweet" 293 | msgstr "Envoyer un tweet" 294 | 295 | #: src/help.py:56 296 | #, fuzzy 297 | msgid "Retweet selected tweet" 298 | msgstr "Retweeter le tweet selectionné" 299 | 300 | #: src/help.py:57 301 | #, fuzzy 302 | msgid "Retweet selected tweet, but edit first" 303 | msgstr "Retweeter le tweet selectionné, et l'éditer" 304 | 305 | #: src/help.py:58 306 | #, fuzzy 307 | msgid "Reply to selected tweet" 308 | msgstr "Répond au tweet selectionné" 309 | 310 | #: src/help.py:59 311 | #, fuzzy 312 | msgid "Send direct message" 313 | msgstr "Envoyer un message direct" 314 | 315 | #: src/help.py:60 316 | #, fuzzy 317 | msgid "Delete selected tweet (must be yours)" 318 | msgstr "Effacer le tweet selectionné, il doit être le votre" 319 | 320 | #: src/help.py:63 321 | #, fuzzy 322 | msgid "Follow selected twitter" 323 | msgstr "Suivre le twitter selectionné" 324 | 325 | #: src/help.py:64 326 | #, fuzzy 327 | msgid "Unfollow selected twitter" 328 | msgstr "Laisser le twitter selectionné" 329 | 330 | #: src/help.py:65 331 | msgid "Follow a twitter" 332 | msgstr "Suivre un twitter" 333 | 334 | #: src/help.py:66 335 | msgid "Unfollow a twitter" 336 | msgstr "Laisser un twitter" 337 | 338 | #: src/help.py:68 339 | msgid "Others" 340 | msgstr "Autres" 341 | 342 | #: src/help.py:69 343 | msgid "Leave Tyrs" 344 | msgstr "Quitter Tyrs" 345 | 346 | #: src/help.py:70 347 | #, fuzzy 348 | msgid "Open URL in browser" 349 | msgstr "Ouvrir une url dans votre navigateur" 350 | 351 | #: src/help.py:71 352 | #, fuzzy 353 | msgid "Redraw the screen" 354 | msgstr "Forcer l'écran à se rafraîchir" 355 | 356 | #: src/help.py:72 357 | msgid "Bookmark selected tweet" 358 | msgstr "Ajouter le tweet séléctionné à vos favoris" 359 | 360 | #: src/help.py:73 361 | msgid "Go to favorite timeline" 362 | msgstr "Timeline des tweets favoris" 363 | 364 | #: src/help.py:74 365 | msgid "Delete an favorite tweet" 366 | msgstr "Supprime le tweet séléctionné de vos favoris" 367 | 368 | #: src/help.py:80 369 | msgid "Open image in browser" 370 | msgstr "Ouvrir l'image dans votre navigateur" 371 | 372 | #: src/help.py:82 373 | msgid "Open thread seltected" 374 | msgstr "Ouvre les réponses du tweet selectionné" 375 | 376 | #: src/help.py:91 377 | msgid "Name" 378 | msgstr "Nom" 379 | 380 | #: src/help.py:92 381 | msgid "Key" 382 | msgstr "Touche" 383 | 384 | #: src/help.py:94 385 | msgid "Description" 386 | msgstr "Description" 387 | 388 | -------------------------------------------------------------------------------- /po/de.po: -------------------------------------------------------------------------------- 1 | # Tyrs 2 | # Copyright (C) 2012 Poapfel 3 | # This file is distributed under the same license as the Tyrs package. 4 | # Poapfel 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: 0.3.1\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2012-03-23 18:56+0100\n" 11 | "PO-Revision-Date: 2012-03-23 18:56+0100\n" 12 | "Last-Translator: \n" 13 | "Language-Team: German\n" 14 | "Language: de\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 19 | 20 | #: src/message.py:22 21 | msgid "Updating timeline..." 22 | msgstr "Aktualisiere Timeline..." 23 | 24 | #: src/message.py:23 25 | #, fuzzy 26 | msgid "Couldn't retrieve tweets" 27 | msgstr "Tweets konnten nicht geladen werden" 28 | 29 | #: src/message.py:26 30 | #, fuzzy 31 | msgid "Your tweet was sent" 32 | msgstr "Dein Tweet wurde gesendet" 33 | 34 | #: src/message.py:27 35 | #, fuzzy 36 | msgid "Couldn't send tweet" 37 | msgstr "Tweet konnte nicht gesendet werden" 38 | 39 | #: src/message.py:30 40 | #, fuzzy 41 | msgid "Your retweet was sent" 42 | msgstr "Dein Retweet wurde versendet" 43 | 44 | #: src/message.py:31 45 | #, fuzzy 46 | msgid "Couldn't send retweet" 47 | msgstr "Retweet konnte nicht versendet werden" 48 | 49 | #: src/message.py:34 50 | #, fuzzy 51 | msgid "You have deleted the tweet" 52 | msgstr "Du hast den Tweet bereits gelöscht" 53 | 54 | #: src/message.py:35 55 | #, fuzzy 56 | msgid "Couldn't delete tweet" 57 | msgstr "Tweet konnte nicht gelöscht werden" 58 | 59 | #: src/message.py:38 60 | #, fuzzy 61 | msgid "The tweet was added to favorites list" 62 | msgstr "Der Tweet wurde zu deinen Favoriten hinzugefügt" 63 | 64 | #: src/message.py:39 65 | #, fuzzy 66 | msgid "Couldn't add tweet to favorites list" 67 | msgstr "Tweet konnte nicht zu deinen Favoriten hinzugefügt werden" 68 | 69 | #: src/message.py:42 70 | #, fuzzy 71 | msgid "Tweet was removed from favorites list" 72 | msgstr "Der Tweet wurde von deinen Favoriten entfernt" 73 | 74 | #: src/message.py:43 75 | #, fuzzy 76 | msgid "Couldn't delete tweet on favorites list" 77 | msgstr "Der Tweet konnte nicht von deinen Favoriten entfernt werden" 78 | 79 | #: src/message.py:46 80 | #, fuzzy 81 | msgid "Direct message was sent" 82 | msgstr "Direkt Nachricht wurde versendet" 83 | 84 | #: src/message.py:47 85 | #, fuzzy 86 | msgid "Couldn't send direct message" 87 | msgstr "Direkt Nachricht konnte nicht versendet werden" 88 | 89 | #: src/message.py:50 90 | #, python-format 91 | msgid "You are now following %s" 92 | msgstr "Du folgst jetzt %s" 93 | 94 | #: src/message.py:51 95 | #, fuzzy, python-format 96 | msgid "Couldn't follow %s" 97 | msgstr "Konnte %s nicht folgen" 98 | 99 | #: src/message.py:54 100 | #, fuzzy, python-format 101 | msgid "You are not following %s anymore" 102 | msgstr "Du folgst %s jetzt nicht mehr" 103 | 104 | #: src/message.py:55 105 | #, fuzzy, python-format 106 | msgid "Couldn't stop following %s" 107 | msgstr "Konnte %s nicht entfolgen" 108 | 109 | #: src/message.py:58 110 | #, python-format 111 | msgid "Search results for %s" 112 | msgstr "Suchergebnisse für %s" 113 | 114 | #: src/message.py:59 115 | #, fuzzy, python-format 116 | msgid "Couldn't search for %s" 117 | msgstr "Konnte nicht nach %s suchen" 118 | 119 | #: src/message.py:86 120 | msgid "Couldn't find any profile." 121 | msgstr "Es wurde kein Profil gefunden" 122 | 123 | #: src/message.py:88 124 | #, fuzzy, python-format 125 | msgid "It should reside in: %s" 126 | msgstr "Es sollte sich in %s befinden" 127 | 128 | #: src/message.py:89 129 | #, fuzzy 130 | msgid "If you want to setup a new account, then follow these steps" 131 | msgstr "Wenn du einen neuen account erstellen möchtest, dann folge diesen schritten" 132 | 133 | #: src/message.py:90 134 | msgid "If you want to skip this, just press return or ctrl-C." 135 | msgstr "Wenn du dies überspringen möchtest drücke einfach strg-C." 136 | 137 | #: src/message.py:94 138 | msgid "Which service do you want to use?" 139 | msgstr "Welchen Dienst möchtest du benutzen?" 140 | 141 | #: src/message.py:103 142 | #, fuzzy 143 | msgid "" 144 | "Which root url do you want? (leave blank for default, https://identi.ca/api)" 145 | msgstr "" 146 | "Welche root url möchtest du benutzen? (gebe nichts an um die Standart Einstellungen zu verwenden, https://identi.ca/api)" 147 | 148 | #: src/config.py:125 tyrs/config.py:137 149 | msgid "Your choice? > " 150 | msgstr "Deine Wahl? > " 151 | 152 | #: src/config.py:249 153 | #, python-format 154 | msgid "The param \"%s\" does not exist for bold colors" 155 | msgstr "Der parameter \"%s\" exestiert nicht für fettgedruckte farben" 156 | 157 | #: src/config.py:286 158 | msgid "Requesting temp token from " 159 | msgstr "Erfrage temp token von " 160 | 161 | #: src/config.py:291 162 | msgid "Invalid respond from " 163 | msgstr "Invalide antwort von " 164 | 165 | #: src/config.py:292 166 | #, python-format 167 | msgid " requesting temp token: %s" 168 | msgstr " erfrage temp token: %s" 169 | 170 | #: src/config.py:297 171 | #, fuzzy 172 | msgid "Please visit the following page to retrieve pin code needed" 173 | msgstr "Bitte besuche die folgende seite um den benötigten pin code in erfahrungen zu bringen" 174 | 175 | #: src/config.py:298 176 | #, fuzzy 177 | msgid "to obtain an Authentication Token:" 178 | msgstr "Um einen Authentifizierung Token zu erhalten:" 179 | 180 | #: src/config.py:309 181 | msgid "Generating and signing request for an access token" 182 | msgstr "Generiere anfrage für einen acess token" 183 | 184 | #: src/config.py:317 185 | #, fuzzy, python-format 186 | msgid "Request for access token failed: %s" 187 | msgstr "Erfragung vom acess token ist fehlgeschlagen: %s" 188 | 189 | #: src/config.py:330 190 | #, fuzzy 191 | msgid "Error creating directory .config/tyrs" 192 | msgstr "Fehler bei der erstellung des Ordners .config/tyrs" 193 | 194 | #: src/config.py:342 195 | msgid "your account has been saved" 196 | msgstr "dein account wurde gespeichert" 197 | 198 | #: src/editor.py:139 199 | msgid "What's up?" 200 | msgstr "Was ist los?" 201 | 202 | #: src/editor.py:142 203 | msgid "Entry a name" 204 | msgstr "Gebe einen Namen ein" 205 | 206 | #: src/editor.py:145 207 | msgid "Search for something?" 208 | msgstr "Möchtest du nach etwas suchen?" 209 | 210 | #: src/help.py:36 211 | msgid "Navigation" 212 | msgstr "Navigation" 213 | 214 | #: src/help.py:37 215 | #, fuzzy 216 | msgid "Go up one tweet" 217 | msgstr "Gehe einen Tweet herauf" 218 | 219 | #: src/help.py:38 220 | msgid "Go down one tweet" 221 | msgstr "Gehe einen Tweet herunter" 222 | 223 | #: src/help.py:39 224 | #, fuzzy 225 | msgid "Go to top of screen" 226 | msgstr "Gehe zum Anfang des Screens" 227 | 228 | #: src/help.py:40 229 | #, fuzzy 230 | msgid "Go to bottom of screen" 231 | msgstr "Gehe zum Ende des Screens" 232 | 233 | #: src/help.py:42 234 | msgid "Timelines" 235 | msgstr "Timelines" 236 | 237 | #: src/help.py:43 238 | #, fuzzy 239 | msgid "Go left on the timeline's bar" 240 | msgstr "Gehe auf der Timeline bar einen Schritt nach links" 241 | 242 | #: src/help.py:44 243 | #, fuzzy 244 | msgid "Go right on the timeline's bar" 245 | msgstr "Gehe auf der Timeline bar einen Schritt nach rechts" 246 | 247 | #: src/help.py:45 248 | #, fuzzy 249 | msgid "Refresh current timeline" 250 | msgstr "Aktualisiere die momentane Timeline" 251 | 252 | #: src/help.py:46 253 | #, fuzzy 254 | msgid "Clear all but last tweet in timeline" 255 | msgstr "Lösche alle außer den letzten Tweet in deiner Timeline" 256 | 257 | #: src/help.py:47 258 | #, fuzzy 259 | msgid "Go to home timeline" 260 | msgstr "Gehe zur haupt Timeline" 261 | 262 | #: src/help.py:48 263 | #, fuzzy 264 | msgid "Go to mentions timeline" 265 | msgstr "Gehe zur mentions Timeline" 266 | 267 | #: src/help.py:49 268 | #, fuzzy 269 | msgid "Go to direct message timeline" 270 | msgstr "Gehe zur direkt Nachrichten Timeline" 271 | 272 | #: src/help.py:50 273 | msgid "Search for term and show resulting timeline" 274 | msgstr "Suche nach einem Begriff und zeige die Ergebnisse in der Timeline" 275 | 276 | #: src/help.py:51 277 | #, fuzzy 278 | msgid "Show somebody's public timeline" 279 | msgstr "Zeige die öffentliche Timeline von jemanden anderem" 280 | 281 | #: src/help.py:52 282 | #, fuzzy 283 | msgid "Show your public timeline" 284 | msgstr "Zeige deine öffentliche Timeline" 285 | 286 | #: src/help.py:54 287 | msgid "Tweets" 288 | msgstr "Tweets" 289 | 290 | #: src/help.py:55 291 | msgid "Send a tweet" 292 | msgstr "Sende einen Tweet" 293 | 294 | #: src/help.py:56 295 | #, fuzzy 296 | msgid "Retweet selected tweet" 297 | msgstr "Retweete den ausgewählten Tweet" 298 | 299 | #: src/help.py:57 300 | #, fuzzy 301 | msgid "Retweet selected tweet, but edit first" 302 | msgstr "Retweete den ausgewählten tweet aber bearbeite in vorher" 303 | 304 | #: src/help.py:58 305 | #, fuzzy 306 | msgid "Reply to selected tweet" 307 | msgstr "Antworte auf den ausgewählten Tweet" 308 | 309 | #: src/help.py:59 310 | #, fuzzy 311 | msgid "Send direct message" 312 | msgstr "Sende eine direkt Nachricht" 313 | 314 | #: src/help.py:60 315 | #, fuzzy 316 | msgid "Delete selected tweet (must be yours)" 317 | msgstr "Lösche den ausgewählten Tweet (es muss dein Tweet sein)" 318 | 319 | #: src/help.py:63 320 | #, fuzzy 321 | msgid "Follow selected twitter" 322 | msgstr "Folge dem ausgewählten Twitter-Nutzer" 323 | 324 | #: src/help.py:64 325 | #, fuzzy 326 | msgid "Unfollow selected twitter" 327 | msgstr "Entfolge den ausgewählten Twitter-Nutzer" 328 | 329 | #: src/help.py:65 330 | msgid "Follow a twitter" 331 | msgstr "Folge einem Twitter-Nutzer" 332 | 333 | #: src/help.py:66 334 | msgid "Unfollow a twitter" 335 | msgstr "Entfolge einem Twitter-Nutzer" 336 | 337 | #: src/help.py:68 338 | msgid "Others" 339 | msgstr "Andere" 340 | 341 | #: src/help.py:69 342 | msgid "Leave Tyrs" 343 | msgstr "Schließe Tyrs" 344 | 345 | #: src/help.py:70 346 | #, fuzzy 347 | msgid "Open URL in browser" 348 | msgstr "Öffne die URL im Browser" 349 | 350 | #: src/help.py:71 351 | #, fuzzy 352 | msgid "Redraw the screen" 353 | msgstr "Erstelle den Screen erneut" 354 | 355 | #: src/help.py:72 356 | msgid "Bookmark selected tweet" 357 | msgstr "Favorisieren den ausgewählten Tweet" 358 | 359 | #: src/help.py:73 360 | msgid "Go to favorite timeline" 361 | msgstr "Gehe zur favoriten Timeline" 362 | 363 | #: src/help.py:74 364 | msgid "Delete an favorite tweet" 365 | msgstr "Lösche einen favorisierten Tweet" 366 | 367 | #: src/help.py:80 368 | msgid "Open image in browser" 369 | msgstr "Öffne das Bild im Browser" 370 | 371 | #: src/help.py:82 372 | msgid "Open thread seltected" 373 | msgstr "Öffne den ausgewählten Thread" 374 | 375 | #: src/help.py:91 376 | msgid "Name" 377 | msgstr "Name" 378 | 379 | #: src/help.py:92 380 | msgid "Key" 381 | msgstr "Key" 382 | 383 | #: src/help.py:94 384 | msgid "Description" 385 | msgstr "Beschreibung" 386 | 387 | -------------------------------------------------------------------------------- /src/tyrs/widget.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import re 17 | import tyrs 18 | import time 19 | import urwid 20 | from utils import html_unescape, encode, get_source, get_urls, get_exact_nick 21 | 22 | class HeaderWidget(urwid.WidgetWrap): 23 | 24 | def __init__(self): 25 | self.api = tyrs.container['api'] 26 | self.interface = tyrs.container['interface'] 27 | self.timelines = tyrs.container['timelines'] 28 | self.buffer = self.interface.buffer 29 | flash = self.set_flash() 30 | activities = self.set_activities() 31 | w = urwid.Columns([flash, ('fixed', 20, activities)]) 32 | self.__super.__init__(w) 33 | 34 | def set_flash(self): 35 | msg = '' 36 | level = 0 37 | msg = self.api.flash_message.get_msg() 38 | color = {0: 'info_msg', 1: 'warn_msg'} 39 | level = self.api.flash_message.level 40 | event_message = urwid.Text(msg) 41 | flash = urwid.AttrWrap(event_message, color[level]) 42 | return flash 43 | 44 | def set_activities(self): 45 | 46 | buffers = ( 47 | 'home', 'mentions', 'direct', 'search', 48 | 'user', 'favorite', 'thread', 'user_retweet', 49 | 'list' 50 | ) 51 | display = { 52 | 'home': 'H', 'mentions': 'M', 'direct': 'D', 53 | 'search': 'S', 'user': 'U', 'favorite': 'F', 54 | 'thread': 'T', 'user_retweet': 'R', 'list': 'L' 55 | } 56 | buff_widget = [] 57 | for b in buffers: 58 | if b == self.buffer: 59 | buff_widget.append(('current_tab', display[b])) 60 | else: 61 | buff_widget.append(('other_tab', display[b])) 62 | if b in ('home', 'mentions', 'direct'): 63 | buff_widget.append(self.get_unread(b)) 64 | 65 | return urwid.Text(buff_widget) 66 | 67 | def get_unread(self, buff): 68 | self.select_current_timeline().all_read() 69 | unread = self.timelines[buff].unread 70 | if unread == 0: 71 | color = 'read' 72 | else: 73 | color = 'unread' 74 | return [('read', ':'), (color , str(unread)), ' '] 75 | 76 | def select_current_timeline(self): 77 | return self.timelines[self.buffer] 78 | 79 | class StatusWidget (urwid.WidgetWrap): 80 | 81 | def __init__ (self, id, status): 82 | self.regex_retweet = re.compile('^RT @\w+:') 83 | self.conf = tyrs.container['conf'] 84 | self.api = tyrs.container['api'] 85 | self.set_date() 86 | self.buffer = tyrs.container['interface'].buffer 87 | self.is_retweet(status) 88 | self.id = id 89 | self.status = status 90 | status_content = urwid.Padding( 91 | urwid.AttrWrap(urwid.Text(self.get_text(status)), 'body'), left=1, right=1) 92 | w = urwid.AttrWrap(TitleLineBox(status_content, title=self.get_header(status)), 'line', 'focus') 93 | self.__super.__init__(w) 94 | 95 | def selectable (self): 96 | return True 97 | 98 | def keypress(self, size, key): 99 | return key 100 | 101 | def get_text(self, status): 102 | result = [] 103 | text = html_unescape(status.text.replace('\n', ' ')) 104 | if status.rt: 105 | text = text.split(':')[1:] 106 | text = ':'.join(text) 107 | 108 | if hasattr(status, 'retweeted_status'): 109 | if hasattr(status.retweeted_status, 'text') \ 110 | and len(status.retweeted_status.text) > 0: 111 | text = status.retweeted_status.text 112 | 113 | myself = self.api.myself.screen_name 114 | 115 | words = text.split(' ') 116 | for word in words: 117 | if word != '': 118 | word += ' ' 119 | # The word is an HASHTAG ? '#' 120 | if word[0] == '#': 121 | result.append(('hashtag', word)) 122 | elif word[0] == '@': 123 | ## The AT TAG is, @myself 124 | if word == '@%s ' % myself or word == '@%s: ' % myself: 125 | result.append(('highlight_nick', word)) 126 | ## @anyone 127 | else: 128 | result.append(('attag', word)) 129 | tyrs.container['completion'].add(get_exact_nick(word)) 130 | 131 | else: 132 | result.append(word) 133 | return result 134 | 135 | def get_header(self, status): 136 | retweeted = '' 137 | reply = '' 138 | retweet_count = '' 139 | retweeter = '' 140 | source = self.get_source(status) 141 | nick = self.get_nick(status) 142 | timer = self.get_time(status) 143 | 144 | if self.is_reply(status): 145 | reply = u' \u2709' 146 | if status.rt: 147 | retweeted = u" \u267b " 148 | retweeter = nick 149 | nick = self.origin_of_retweet(status) 150 | 151 | if self.get_retweet_count(status): 152 | retweet_count = str(self.get_retweet_count(status)) 153 | 154 | tyrs.container['completion'].add(get_exact_nick(nick)) 155 | header_template = self.conf.params['header_template'] 156 | header = unicode(header_template).format( 157 | time = timer, 158 | nick = nick, 159 | reply = reply, 160 | retweeted = retweeted, 161 | source = source, 162 | retweet_count = retweet_count, 163 | retweeter = retweeter 164 | ) 165 | 166 | return encode(header) 167 | 168 | def set_date(self): 169 | self.date = time.strftime("%d %b", time.gmtime()) 170 | 171 | def get_time(self, status): 172 | '''Handle the time format given by the api with something more 173 | readeable 174 | @param date: full iso time format 175 | @return string: readeable time 176 | ''' 177 | if self.conf.params['relative_time'] == 1 and self.buffer != 'direct': 178 | try: 179 | result = status.GetRelativeCreatedAt() 180 | except AttributeError: 181 | return '' 182 | else: 183 | hour = time.gmtime(status.GetCreatedAtInSeconds() - time.altzone) 184 | result = time.strftime('%H:%M', hour) 185 | if time.strftime('%d %b', hour) != self.date: 186 | result += time.strftime(' - %d %b', hour) 187 | 188 | return result 189 | 190 | def get_source(self, status): 191 | source = '' 192 | if hasattr(status, 'source'): 193 | source = get_source(status.source) 194 | 195 | return source 196 | 197 | def get_nick(self, status): 198 | if hasattr(status, 'user'): 199 | nick = status.user.screen_name 200 | else: 201 | #Used for direct messages 202 | nick = status.sender_screen_name 203 | 204 | return nick 205 | 206 | def get_retweet_count(self, status): 207 | if hasattr(status, 'retweet_count'): 208 | return status.retweet_count 209 | 210 | def is_retweet(self, status): 211 | status.rt = self.regex_retweet.match(status.text) 212 | return status.rt 213 | 214 | def is_reply(self, status): 215 | if hasattr(status, 'in_reply_to_screen_name'): 216 | reply = status.in_reply_to_screen_name 217 | if reply: 218 | return True 219 | return False 220 | 221 | def origin_of_retweet(self, status): 222 | '''When its a retweet, return the first person who tweet it, 223 | not the retweeter 224 | ''' 225 | origin = status.text 226 | origin = origin[4:] 227 | origin = origin.split(':')[0] 228 | origin = str(origin) 229 | return origin 230 | 231 | 232 | 233 | class TitleLineBox(urwid.WidgetDecoration, urwid.WidgetWrap): 234 | def __init__(self, original_widget, title=''): 235 | """Draw a line around original_widget.""" 236 | 237 | 238 | self.color = 'header' 239 | if int(urwid.__version__[0]) == 1: 240 | urwid.utf8decode = self.utf8decode 241 | 242 | tlcorner=None; tline=None; lline=None 243 | trcorner=None; blcorner=None; rline=None 244 | bline=None; brcorner=None 245 | 246 | def use_attr( a, t ): 247 | if a is not None: 248 | t = urwid.AttrWrap(t, a) 249 | return t 250 | 251 | tline = use_attr( tline, urwid.Columns([ 252 | ('fixed', 2, urwid.Divider(urwid.utf8decode("─"))), 253 | ('fixed', len(title), urwid.AttrWrap(urwid.Text(title), self.color)), 254 | urwid.Divider(urwid.utf8decode("─"))])) 255 | bline = use_attr( bline, urwid.Divider(urwid.utf8decode("─"))) 256 | lline = use_attr( lline, urwid.SolidFill(urwid.utf8decode("│"))) 257 | rline = use_attr( rline, urwid.SolidFill(urwid.utf8decode("│"))) 258 | tlcorner = use_attr( tlcorner, urwid.Text(urwid.utf8decode("┌"))) 259 | trcorner = use_attr( trcorner, urwid.Text(urwid.utf8decode("┐"))) 260 | blcorner = use_attr( blcorner, urwid.Text(urwid.utf8decode("└"))) 261 | brcorner = use_attr( brcorner, urwid.Text(urwid.utf8decode("┘"))) 262 | top = urwid.Columns([ ('fixed', 1, tlcorner), 263 | tline, ('fixed', 1, trcorner) ]) 264 | middle = urwid.Columns( [('fixed', 1, lline), 265 | original_widget, ('fixed', 1, rline)], box_columns = [0,2], 266 | focus_column = 1) 267 | bottom = urwid.Columns([ ('fixed', 1, blcorner), 268 | bline, ('fixed', 1, brcorner) ]) 269 | pile = urwid.Pile([('flow',top),middle,('flow',bottom)], 270 | focus_item = 1) 271 | 272 | urwid.WidgetDecoration.__init__(self, original_widget) 273 | urwid.WidgetWrap.__init__(self, pile) 274 | 275 | def utf8decode(self, string): 276 | return string 277 | 278 | -------------------------------------------------------------------------------- /src/tyrs/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import os 17 | import sys 18 | import curses 19 | import logging 20 | import message 21 | import constant 22 | import ConfigParser 23 | import curses.ascii 24 | import oauth2 as oauth 25 | from utils import encode 26 | try: 27 | from urlparse import parse_qsl 28 | except ImportError: 29 | from cgi import parse_qsl 30 | 31 | class Config(object): 32 | 33 | def __init__(self, args): 34 | self.init_config() 35 | self.home = os.environ['HOME'] 36 | self.get_xdg_config() 37 | self.get_browser() 38 | # generate the config file 39 | if args.generate_config != None: 40 | self.generate_config_file(args.generate_config) 41 | sys.exit(0) 42 | 43 | self.set_path(args) 44 | self.check_for_default_config() 45 | self.conf = ConfigParser.RawConfigParser() 46 | self.conf.read(self.config_file) 47 | if not os.path.isfile(self.token_file): 48 | self.new_account() 49 | else: 50 | self.parse_token() 51 | 52 | self.parse_config() 53 | 54 | def init_config(self): 55 | self.token = constant.token 56 | self.keys = constant.key 57 | self.params = constant.params 58 | self.filter = constant.filter 59 | self.palette = constant.palette 60 | 61 | def get_xdg_config(self): 62 | try: 63 | self.xdg_config = os.environ['XDG_CONFIG_HOME'] 64 | except: 65 | self.xdg_config = self.home+'/.config' 66 | 67 | def get_browser(self): 68 | try: 69 | self.browser = os.environ['BROWSER'] 70 | except: 71 | self.browser = '' 72 | 73 | def check_for_default_config(self): 74 | default_dir = '/tyrs' 75 | default_file = '/tyrs/tyrs.cfg' 76 | if not os.path.isfile(self.xdg_config + default_file): 77 | if not os.path.exists(self.xdg_config + default_dir): 78 | try: 79 | os.makedirs(self.xdg_config + default_dir) 80 | except: 81 | print encode(_('Couldn\'t create the directory in %s/tyrs')) % self.xdg_config 82 | self.generate_config_file(self.xdg_config + default_file) 83 | 84 | 85 | def generate_config_file(self, config_file): 86 | conf = ConfigParser.RawConfigParser() 87 | conf.read(config_file) 88 | 89 | # COLOR 90 | conf.add_section('colors') 91 | for c in self.palette: 92 | conf.set('colors', c[0], c[1]) 93 | # KEYS 94 | conf.add_section('keys') 95 | for k in self.keys: 96 | conf.set('keys', k, self.keys[k]) 97 | # PARAMS 98 | conf.add_section('params') 99 | for p in self.params: 100 | if self.params[p] == True: 101 | value = 1 102 | elif self.params[p] == False: 103 | value = 0 104 | elif self.params[p] == None: 105 | continue 106 | else: 107 | value = self.params[p] 108 | 109 | conf.set('params', p, value) 110 | 111 | with open(config_file, 'wb') as config: 112 | conf.write(config) 113 | 114 | print encode(_('Generating configuration file in %s')) % config_file 115 | 116 | def set_path(self, args): 117 | # Default config path set 118 | if self.xdg_config != '': 119 | self.tyrs_path = self.xdg_config + '/tyrs/' 120 | else: 121 | self.tyrs_path = self.home + '/.config/tyrs/' 122 | # Setup the token file 123 | self.token_file = self.tyrs_path + 'tyrs.tok' 124 | if args.account != None: 125 | self.token_file += '.' + args.account 126 | # Setup the config file 127 | self.config_file = self.tyrs_path + 'tyrs.cfg' 128 | if args.config != None: 129 | self.config_file += '.' + args.config 130 | 131 | def new_account(self): 132 | 133 | choice = self.ask_service() 134 | if choice == '2': 135 | self.ask_root_url() 136 | 137 | self.authorization() 138 | self.createTokenFile() 139 | 140 | def ask_service(self): 141 | message.print_ask_service(self.token_file) 142 | choice = raw_input(encode(_('Your choice? > '))) 143 | 144 | if choice == '1': 145 | self.service = 'twitter' 146 | elif choice == '2': 147 | self.service = 'identica' 148 | else: 149 | sys.exit(1) 150 | return choice 151 | 152 | def ask_root_url(self): 153 | message.print_ask_root_url() 154 | url = raw_input(encode(_('Your choice? > '))) 155 | if url == '': 156 | self.base_url = 'https://identi.ca/api' 157 | else: 158 | self.base_url = url 159 | 160 | def parse_token(self): 161 | token = ConfigParser.RawConfigParser() 162 | token.read(self.token_file) 163 | if token.has_option('token', 'service'): 164 | self.service = token.get('token', 'service') 165 | else: 166 | self.service = 'twitter' 167 | 168 | if token.has_option('token', 'base_url'): 169 | self.base_url = token.get('token', 'base_url') 170 | 171 | self.oauth_token = token.get('token', 'oauth_token') 172 | self.oauth_token_secret = token.get('token', 'oauth_token_secret') 173 | 174 | def parse_config(self): 175 | self.parse_color() 176 | self.parse_keys() 177 | self.parse_params() 178 | self.parse_filter() 179 | self.init_logger() 180 | 181 | def parse_color(self): 182 | for i, c in enumerate(self.palette): 183 | if self.conf.has_option('colors', c[0]): 184 | self.palette[i][1] = (self.conf.get('colors', c[0])) 185 | 186 | def parse_keys(self): 187 | for key in self.keys: 188 | if self.conf.has_option('keys', key): 189 | self.keys[key] = self.conf.get('keys', key) 190 | else: 191 | self.keys[key] = self.keys[key] 192 | 193 | def char_value(self, ch): 194 | if ch[0] == '^': 195 | i = 0 196 | while i <= 31: 197 | if curses.ascii.unctrl(i) == ch.upper(): 198 | return i 199 | i +=1 200 | return ord(ch) 201 | 202 | def parse_params(self): 203 | 204 | # refresh (in minutes) 205 | if self.conf.has_option('params', 'refresh'): 206 | self.params['refresh'] = int(self.conf.get('params', 'refresh')) 207 | 208 | if self.conf.has_option('params', 'box_position'): 209 | self.params['refresh'] = int(self.conf.get('params', 'box_position')) 210 | 211 | # tweet_border 212 | if self.conf.has_option('params', 'tweet_border'): 213 | self.params['tweet_border'] = int(self.conf.get('params', 'tweet_border')) 214 | 215 | # Relative_time 216 | if self.conf.has_option('params', 'relative_time'): 217 | self.params['relative_time'] = int(self.conf.get('params', 'relative_time')) 218 | 219 | # Retweet_By 220 | if self.conf.has_option('params', 'retweet_by'): 221 | self.params['retweet_by'] = int(self.conf.get('params', 'retweet_by')) 222 | 223 | # Openurl_command 224 | if self.conf.has_option('params', 'openurl_command'): 225 | self.params['openurl_command'] = self.conf.get('params', 226 | 'openurl_command') 227 | elif self.browser != '': 228 | self.params['openurl_command'] = self.browser + ' %s' 229 | 230 | if self.conf.has_option('params', 'open_image_command'): 231 | self.params['open_image_command'] = self.conf.get('params', 232 | 'open_image_command') 233 | 234 | # Transparency 235 | if self.conf.has_option('params', 'transparency'): 236 | if int(self.conf.get('params', 'transparency')) == 0: 237 | self.params['transparency'] = False 238 | # Compress display 239 | if self.conf.has_option('params', 'compact'): 240 | if int(self.conf.get('params', 'compact')) == 1: 241 | self.params['compact'] = True 242 | # Help bar 243 | if self.conf.has_option('params', 'help'): 244 | if int(self.conf.get('params', 'help')) == 0: 245 | self.params['help'] = False 246 | 247 | if self.conf.has_option('params', 'margin'): 248 | self.params['margin'] = int(self.conf.get('params', 'margin')) 249 | 250 | if self.conf.has_option('params', 'padding'): 251 | self.params['padding'] = int(self.conf.get('params', 'padding')) 252 | 253 | if self.conf.has_option('params', 'old_skool_border'): 254 | if int(self.conf.get('params', 'old_skool_border')) == 1: 255 | self.params['old_skool_border'] = True 256 | 257 | if self.conf.has_option('params', 'consumer_key'): 258 | self.token['identica']['consumer_key'] = self.conf.get('params', 'consumer_key') 259 | 260 | if self.conf.has_option('params', 'consumer_secret'): 261 | self.token['identica']['consumer_secret'] = self.conf.get('params', 'consumer_secret') 262 | 263 | if self.conf.has_option('params', 'logging_level'): 264 | self.params['logging_level'] = self.conf.get('params', 'logging_level') 265 | 266 | if self.conf.has_option('params', 'url_shorter'): 267 | shortener = self.params['url_shorter'] = self.conf.get('params', 'url_shorter') 268 | if shortener == 'googl': 269 | self.check_google_tokens() 270 | 271 | if self.conf.has_option('params', 'header_template'): 272 | self.params['header_template'] = self.conf.get('params', 'header_template') 273 | 274 | if self.conf.has_option('params', 'proxy'): 275 | self.params['proxy'] = self.conf.get('params', 'proxy') 276 | 277 | if self.conf.has_option('params', 'beep'): 278 | self.params['beep'] = self.conf.getboolean('params', 'beep') 279 | 280 | def check_google_tokens(self): 281 | try: 282 | from shorter.googl import GooglUrlShorter 283 | except ImportError: 284 | print 'please install google-api-python-client and python-gflags' 285 | sys.exit(1) 286 | GooglUrlShorter().register_token() 287 | 288 | 289 | def parse_filter(self): 290 | 291 | if self.conf.has_option('filter', 'activate'): 292 | if int(self.conf.get('filter', 'activate')) == 1: 293 | self.filter['activate'] = True 294 | 295 | if self.conf.has_option('filter', 'myself'): 296 | if int(self.conf.get('filter', 'myself')) == 1: 297 | self.filter['myself'] = True 298 | 299 | if self.conf.has_option('filter', 'behavior'): 300 | self.filter['behavior'] = self.conf.get('filter', 'behavior') 301 | 302 | if self.conf.has_option('filter', 'except'): 303 | self.filter['except'] = self.conf.get('filter', 'except').split(' ') 304 | 305 | def init_logger(self): 306 | log_file = self.xdg_config + '/tyrs/tyrs.log' 307 | lvl = self.init_logger_level() 308 | 309 | logging.basicConfig( 310 | filename=log_file, 311 | level=lvl, 312 | format='%(asctime)s %(levelname)s - %(message)s', 313 | datefmt='%d/%m/%Y %H:%M:%S', 314 | ) 315 | logging.info('Tyrs starting...') 316 | 317 | def init_logger_level(self): 318 | lvl = int(self.params['logging_level']) 319 | if lvl == 1: 320 | return logging.DEBUG 321 | elif lvl == 2: 322 | return logging.INFO 323 | elif lvl == 3: 324 | return logging.WARNING 325 | elif lvl == 4: 326 | return logging.ERROR 327 | 328 | def authorization(self): 329 | ''' This function from python-twitter developers ''' 330 | # Copyright 2007 The Python-Twitter Developers 331 | # 332 | # Licensed under the Apache License, Version 2.0 (the "License"); 333 | # you may not use this file except in compliance with the License. 334 | # You may obtain a copy of the License at 335 | # 336 | # http://www.apache.org/licenses/LICENSE-2.0 337 | # 338 | # Unless required by applicable law or agreed to in writing, software 339 | # distributed under the License is distributed on an "AS IS" BASIS, 340 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 341 | # See the License for the specific language governing permissions and 342 | # limitations under the License. 343 | 344 | if self.service == 'twitter': 345 | base_url = 'https://api.twitter.com' 346 | self.base_url = base_url 347 | else: 348 | base_url = self.base_url 349 | 350 | print 'base_url:{0}'.format(base_url) 351 | 352 | 353 | REQUEST_TOKEN_URL = base_url + '/oauth/request_token' 354 | if self.service == 'identica': 355 | if base_url != 'https://identi.ca/api': 356 | self.parse_config() 357 | REQUEST_TOKEN_URL += '?oauth_callback=oob' 358 | 359 | ACCESS_TOKEN_URL = base_url + '/oauth/access_token' 360 | AUTHORIZATION_URL = base_url + '/oauth/authorize' 361 | consumer_key = self.token[self.service]['consumer_key'] 362 | consumer_secret = self.token[self.service]['consumer_secret'] 363 | signature_method_hmac_sha1 = oauth.SignatureMethod_HMAC_SHA1() 364 | oauth_consumer = oauth.Consumer(key=consumer_key, secret=consumer_secret) 365 | oauth_client = oauth.Client(oauth_consumer) 366 | 367 | print encode(_('Requesting temp token from ')) + self.service.capitalize() 368 | 369 | resp, content = oauth_client.request(REQUEST_TOKEN_URL, 'GET') 370 | 371 | if resp['status'] != '200': 372 | print encode(_('Invalid respond from ')) +self.service.capitalize() + encode(_(' requesting temp token: %s')) % str(resp['status']) 373 | else: 374 | request_token = dict(parse_qsl(content)) 375 | 376 | print '' 377 | print encode(_('Please visit the following page to retrieve pin code needed')) 378 | print encode(_('to obtain an Authentication Token:')) 379 | print '' 380 | print '%s?oauth_token=%s' % (AUTHORIZATION_URL, request_token['oauth_token']) 381 | print '' 382 | 383 | pincode = raw_input('Pin code? ') 384 | 385 | token = oauth.Token(request_token['oauth_token'], request_token['oauth_token_secret']) 386 | token.set_verifier(pincode) 387 | 388 | print '' 389 | print encode(_('Generating and signing request for an access token')) 390 | print '' 391 | 392 | oauth_client = oauth.Client(oauth_consumer, token) 393 | resp, content = oauth_client.request(ACCESS_TOKEN_URL, method='POST', body='oauth_verifier=%s' % pincode) 394 | access_token = dict(parse_qsl(content)) 395 | 396 | if resp['status'] != '200': 397 | print 'response:{0}'.format(resp['status']) 398 | print encode(_('Request for access token failed: %s')) % resp['status'] 399 | print access_token 400 | sys.exit() 401 | else: 402 | self.oauth_token = access_token['oauth_token'] 403 | self.oauth_token_secret = access_token['oauth_token_secret'] 404 | 405 | 406 | def createTokenFile(self): 407 | 408 | if not os.path.isdir(self.tyrs_path): 409 | try: 410 | os.mkdir(self.tyrs_path) 411 | except: 412 | print encode(_('Error creating directory .config/tyrs')) 413 | 414 | conf = ConfigParser.RawConfigParser() 415 | conf.add_section('token') 416 | conf.set('token', 'service', self.service) 417 | conf.set('token', 'base_url', self.base_url) 418 | conf.set('token', 'oauth_token', self.oauth_token) 419 | conf.set('token', 'oauth_token_secret', self.oauth_token_secret) 420 | 421 | with open(self.token_file, 'wb') as tokens: 422 | conf.write(tokens) 423 | 424 | print encode(_('your account has been saved')) 425 | 426 | def load_last_read(self): 427 | 428 | try: 429 | conf = ConfigParser.RawConfigParser() 430 | conf.read(self.token_file) 431 | return conf.get('token', 'last_read') 432 | except: 433 | return False 434 | 435 | def save_last_read(self, last_read): 436 | 437 | conf = ConfigParser.RawConfigParser() 438 | conf.read(self.token_file) 439 | conf.set('token', 'last_read', last_read) 440 | 441 | with open(self.token_file, 'wb') as tokens: 442 | conf.write(tokens) 443 | -------------------------------------------------------------------------------- /src/tyrs/tweets.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2011 Nicolas Paris 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program. If not, see . 15 | 16 | import os 17 | import sys 18 | import tyrs 19 | import urwid 20 | import logging 21 | import urllib2 22 | import oauth2 as oauth 23 | from utils import encode 24 | from help import help_bar 25 | from urllib2 import URLError 26 | from message import FlashMessage 27 | from httplib import BadStatusLine 28 | from twitter import Api, TwitterError, Status, _FileCache, List 29 | 30 | try: 31 | import json 32 | except ImportError: 33 | import simplejson as json 34 | 35 | class Tweets(object): 36 | 37 | def __init__(self): 38 | self.conf = tyrs.container['conf'] 39 | self.timelines = tyrs.container['timelines'] 40 | self.search_user = None 41 | self.search_word = None 42 | self.flash_message = FlashMessage() 43 | 44 | def set_interface(self): 45 | self.interface = tyrs.container['interface'] 46 | 47 | def authentication(self): 48 | url = self.get_base_url() 49 | proxify = self.get_proxy() 50 | self.api = ApiPatch( 51 | self.conf.token[self.conf.service]['consumer_key'], 52 | self.conf.token[self.conf.service]['consumer_secret'], 53 | self.conf.oauth_token, 54 | self.conf.oauth_token_secret, 55 | base_url=url, 56 | proxy=proxify 57 | ) 58 | self.set_myself() 59 | 60 | def get_base_url(self): 61 | url = None 62 | if self.conf.service == 'identica': 63 | url = self.conf.base_url 64 | 65 | return url 66 | 67 | def get_proxy(self): 68 | proxy = self.conf.params['proxy'] 69 | if proxy: 70 | return { 71 | 'http': 'http://{0}'.format(proxy), 72 | 'https': 'https://{0}'.format(proxy), 73 | } 74 | else: 75 | return {} 76 | 77 | def set_myself(self): 78 | self.myself = self.api.VerifyCredentials() 79 | self.conf.my_nick = self.myself.screen_name 80 | 81 | def post_tweet(self, tweet, reply_to=None): 82 | self.flash('tweet') 83 | try: 84 | return self.api.PostUpdate(tweet, reply_to) 85 | except TwitterError, e: 86 | self.error(e) 87 | 88 | def retweet(self): 89 | self.flash('retweet') 90 | status = self.interface.current_status() 91 | try: 92 | self.api.PostRetweet(status.id) 93 | except TwitterError, e: 94 | self.error(e) 95 | 96 | def retweet_and_edit(self): 97 | status = self.interface.current_status() 98 | nick = status.user.screen_name 99 | data = 'RT @%s: %s' % (nick, status.text) 100 | self.interface.edit_status('tweet', data) 101 | 102 | #def reply(self): 103 | #status = self.interface.current_status() 104 | #if hasattr(status, 'user'): 105 | #nick = status.user.screen_name 106 | #else: 107 | #self.direct_message() 108 | #data = '@' + nick + ' ' 109 | #tweet = TweetEditor(data).content 110 | #if tweet: 111 | #self.post_tweet(tweet, status.id) 112 | 113 | def destroy(self): 114 | self.flash('destroy') 115 | status = self.interface.current_status() 116 | try: 117 | self.api.DestroyStatus(status.id) 118 | except TwitterError, e: 119 | self.error(e) 120 | 121 | #FIXME! 122 | def direct_message(self): 123 | ''' Two editing box, one for the name, and one for the content''' 124 | nick = self.nick_for_direct_message() 125 | tweet = TweetEditor().content 126 | if tweet: 127 | self.send_direct_message(nick, tweet) 128 | 129 | #FIXME! 130 | def nick_for_direct_message(self): 131 | status = self.interface.current_status() 132 | if hasattr(status, 'user'): 133 | nick = status.user.screen_name 134 | else: 135 | nick = status.sender_screen_name 136 | nick = NickEditor(nick).content 137 | 138 | return nick 139 | 140 | def send_direct_message(self, nick, tweet): 141 | self.flash('direct') 142 | try: 143 | return self.api.PostDirectMessage(nick, tweet) 144 | except TwitterError, e: 145 | self.error(e) 146 | 147 | def do_list(self): 148 | self.interface.edit_status('list') 149 | 150 | def follow(self): 151 | self.interface.edit_status('follow') 152 | 153 | def follow_selected(self): 154 | status = self.interface.current_status() 155 | if self.interface.is_retweet(status): 156 | nick = self.interface.origin_of_retweet(status) 157 | else: 158 | nick = status.user.screen_name 159 | self.create_friendship(nick) 160 | 161 | #def unfollow(self): 162 | #nick = NickEditor().content 163 | #if nick: 164 | #self.destroy_friendship(nick) 165 | 166 | def unfollow_selected(self): 167 | nick = self.interface.current_status().user.screen_name 168 | self.destroy_friendship(nick) 169 | 170 | def create_friendship(self, nick): 171 | self.flash('follow', nick) 172 | try: 173 | self.api.CreateFriendship(nick) 174 | except TwitterError, e: 175 | self.error(e) 176 | 177 | def destroy_friendship(self, nick): 178 | self.flash('unfollow', nick) 179 | try: 180 | self.api.DestroyFriendship(nick) 181 | except TwitterError, e: 182 | self.error(e) 183 | 184 | def set_favorite(self): 185 | self.flash('favorite') 186 | status = self.interface.current_status() 187 | try: 188 | self.api.CreateFavorite(status) 189 | except TwitterError, e: 190 | self.error(e) 191 | 192 | def destroy_favorite(self): 193 | self.flash('favorite_del') 194 | status = self.interface.current_status() 195 | try: 196 | self.api.DestroyFavorite(status) 197 | except TwitterError, e: 198 | self.error(e) 199 | 200 | def get_favorites(self): 201 | self.interface.change_buffer('favorite') 202 | 203 | def update_timeline(self, timeline): 204 | ''' 205 | Retrieves tweets, don't display them 206 | @param the buffer to retreive tweets 207 | ''' 208 | 209 | logging.debug('updating "{0}" timeline'.format(timeline)) 210 | try: 211 | statuses = self.retreive_statuses(timeline) 212 | timeline = self.timelines[timeline] 213 | timeline.append_new_statuses(statuses) 214 | if timeline.unread and self.conf.params['beep']: 215 | self.interface.beep() 216 | 217 | except TwitterError, e: 218 | self.update_error(e) 219 | except BadStatusLine, e: 220 | self.update_error(e) 221 | except ValueError, e: 222 | self.update_error(e) 223 | except URLError, e: 224 | self.update_error(e) 225 | 226 | def update_error(self, err): 227 | logging.error('Updating issue: {0}'.format(err)) 228 | self.flash_message.event = 'update' 229 | self.flash_message.level = 1 230 | self.interface.display_flash_message() 231 | 232 | def retreive_statuses(self, timeline, page=None): 233 | self.flash_message.event = 'update' 234 | self.flash_message.level = 0 235 | self.interface.display_flash_message() 236 | if timeline == 'home': 237 | statuses = self.api.GetFriendsTimeline(retweets=True, page=page) 238 | elif timeline == 'mentions': 239 | statuses = self.api.GetMentions(page=page) 240 | elif timeline == 'user_retweet': 241 | statuses = self.api.GetUserRetweets() 242 | elif timeline == 'search' and self.search_word != '': 243 | statuses = self.api.GetSearch(self.search_word, page=page) 244 | elif timeline == 'direct': 245 | statuses = self.api.GetDirectMessages(page=page) 246 | elif timeline == 'user' and self.search_user != '': 247 | statuses = self.load_user_public_timeline(page=page) 248 | elif timeline == 'favorite': 249 | statuses = self.api.GetFavorites(page=page) 250 | elif timeline == 'thread': 251 | statuses = self.get_thread() 252 | elif timeline == 'list': 253 | statuses = self.api.GetListStatuses(self.myself.screen_name, self.list_slug, per_page='15', page=page) 254 | self.interface.erase_flash_message() 255 | 256 | return statuses 257 | 258 | 259 | def find_public_timeline(self, nick): 260 | if nick and nick != self.search_user: 261 | self.change_search_user(nick) 262 | self.load_user_public_timeline() 263 | self.interface.change_buffer('user') 264 | 265 | def find_current_public_timeline(self): 266 | self.change_search_user(self.interface.current_status().user.screen_name) 267 | self.load_user_public_timeline() 268 | self.interface.change_buffer('user') 269 | 270 | def change_search_user(self, nick): 271 | self.search_user = nick 272 | self.timelines['user'].empty('user') 273 | 274 | def my_public_timeline(self): 275 | self.change_search_user(self.myself.screen_name) 276 | self.load_user_public_timeline() 277 | 278 | def load_user_public_timeline(self, page=None): 279 | if self.search_user: 280 | return self.api.GetUserTimeline(self.search_user, 281 | include_rts=True, page=page) 282 | else: 283 | return [] 284 | 285 | def get_thread(self): 286 | try: 287 | status = self.interface.current_status() 288 | self.timelines['thread'].empty('thread') 289 | self.statuses = [status] 290 | self.build_thread(status) 291 | self.timelines['thread'].append_new_statuses(self.statuses) 292 | self.interface.change_buffer('thread') 293 | except IndexError: 294 | return [] 295 | 296 | def build_thread(self, status): 297 | if status.in_reply_to_status_id: 298 | try: 299 | reply_to = self.api.GetStatus(status.in_reply_to_status_id) 300 | self.statuses.append(reply_to) 301 | self.build_thread(reply_to) 302 | except TwitterError: 303 | pass 304 | 305 | def search(self, content): 306 | self.search_word = content 307 | self.flash('search', self.search_word) 308 | self.timelines['search'].empty('search') 309 | try: 310 | self.timelines['search'].append_new_statuses(self.api.GetSearch(self.search_word)) 311 | self.interface.change_buffer('search') 312 | except TwitterError, e: 313 | self.error(e) 314 | 315 | def list(self, content): 316 | self.list_slug = content 317 | self.flash('list', self.list_slug) 318 | self.timelines['list'].empty('list') 319 | try: 320 | self.timelines['list'].append_new_statuses(self.api.GetListStatuses(self.myself.screen_name,self.list_slug, per_page='15')) 321 | self.interface.change_buffer('list') 322 | except TwitterError, e: 323 | self.error(e) 324 | 325 | def tweet_done(self, content): 326 | self.clean_edit() 327 | urwid.disconnect_signal(self, self.interface.foot, 'done', self.tweet_done) 328 | if content: 329 | self.post_tweet(encode(content)) 330 | 331 | def reply_done(self, content): 332 | self.clean_edit() 333 | urwid.disconnect_signal(self, self.interface.foot, 'done', self.reply_done) 334 | if content: 335 | self.post_tweet(encode(content), self.interface.current_status().id) 336 | 337 | def follow_done(self, content): 338 | self.clean_edit() 339 | urwid.disconnect_signal(self, self.interface.foot, 'done', self.follow_done) 340 | if content: 341 | self.create_friendship(content) 342 | 343 | def unfollow_done(self, content): 344 | self.clean_edit() 345 | urwid.disconnect_signal(self, self.interface.foot, 'done', self.unfollow_done) 346 | if content: 347 | self.destroy_friendship(content) 348 | 349 | def search_done(self, content): 350 | self.clean_edit() 351 | urwid.disconnect_signal(self, self.interface.foot, 'done', self.search_done) 352 | if content: 353 | self.search(encode(content)) 354 | 355 | def public_done(self, content): 356 | self.clean_edit() 357 | urwid.disconnect_signal(self, self.interface.foot, 'done', self.public_done) 358 | if content: 359 | self.find_public_timeline(content) 360 | 361 | def list_done(self, content): 362 | self.clean_edit() 363 | urwid.disconnect_signal(self, self.interface.foot, 'done', self.list_done) 364 | if content: 365 | self.list(encode(content)) 366 | 367 | def clean_edit(self): 368 | footer = help_bar() 369 | self.interface.main_frame.set_focus('body') 370 | self.interface.main_frame.set_footer(footer) 371 | 372 | def flash(self, event, string=None): 373 | self.flash_message.event = event 374 | if string: 375 | self.flash_message.string = string 376 | 377 | def error(self, err=None): 378 | logging.warning('Error catch: {0}'.format(err)) 379 | self.flash_message.warning() 380 | 381 | DEFAULT_CACHE = object() 382 | 383 | class ApiPatch(Api): 384 | 385 | def __init__(self, 386 | consumer_key=None, 387 | consumer_secret=None, 388 | access_token_key=None, 389 | access_token_secret=None, 390 | input_encoding=None, 391 | request_headers=None, 392 | cache=DEFAULT_CACHE, 393 | shortner=None, 394 | base_url=None, 395 | use_gzip_compression=False, 396 | debugHTTP=False, 397 | proxy={} 398 | ): 399 | 400 | 401 | self.SetCache(cache) 402 | self._urllib = urllib2 403 | self._cache_timeout = Api.DEFAULT_CACHE_TIMEOUT 404 | self._input_encoding = input_encoding 405 | self._use_gzip = use_gzip_compression 406 | self._debugHTTP = debugHTTP 407 | self._oauth_consumer = None 408 | self._proxy = proxy 409 | 410 | self._InitializeRequestHeaders(request_headers) 411 | self._InitializeUserAgent() 412 | self._InitializeDefaultParameters() 413 | 414 | if base_url is None: 415 | self.base_url = 'https://api.twitter.com/1' 416 | else: 417 | self.base_url = base_url 418 | 419 | if consumer_key is not None and (access_token_key is None or 420 | access_token_secret is None): 421 | print >> sys.stderr, 'Twitter now requires an oAuth Access Token for API calls.' 422 | print >> sys.stderr, 'If your using this library from a command line utility, please' 423 | print >> sys.stderr, 'run the the included get_access_token.py tool to generate one.' 424 | 425 | raise TwitterError('Twitter requires oAuth Access Token for all API access') 426 | 427 | self.SetCredentials(consumer_key, consumer_secret, access_token_key, access_token_secret) 428 | 429 | def _FetchUrl(self, 430 | url, 431 | post_data=None, 432 | parameters=None, 433 | no_cache=None, 434 | use_gzip_compression=None): 435 | 436 | 437 | # Build the extra parameters dict 438 | extra_params = {} 439 | if self._default_params: 440 | extra_params.update(self._default_params) 441 | if parameters: 442 | extra_params.update(parameters) 443 | 444 | if post_data: 445 | http_method = "POST" 446 | else: 447 | http_method = "GET" 448 | 449 | if self._debugHTTP: 450 | _debug = 1 451 | else: 452 | _debug = 0 453 | 454 | http_handler = self._urllib.HTTPHandler(debuglevel=_debug) 455 | https_handler = self._urllib.HTTPSHandler(debuglevel=_debug) 456 | proxy_handler = self._urllib.ProxyHandler(self._proxy) 457 | 458 | opener = self._urllib.OpenerDirector() 459 | opener.add_handler(http_handler) 460 | opener.add_handler(https_handler) 461 | 462 | if self._proxy: 463 | opener.add_handler(proxy_handler) 464 | 465 | if use_gzip_compression is None: 466 | use_gzip = self._use_gzip 467 | else: 468 | use_gzip = use_gzip_compression 469 | 470 | # Set up compression 471 | if use_gzip and not post_data: 472 | opener.addheaders.append(('Accept-Encoding', 'gzip')) 473 | 474 | if self._oauth_consumer is not None: 475 | if post_data and http_method == "POST": 476 | parameters = post_data.copy() 477 | 478 | req = oauth.Request.from_consumer_and_token(self._oauth_consumer, 479 | token=self._oauth_token, 480 | http_method=http_method, 481 | http_url=url, parameters=parameters) 482 | 483 | req.sign_request(self._signature_method_hmac_sha1, self._oauth_consumer, self._oauth_token) 484 | 485 | headers = req.to_header() 486 | 487 | if http_method == "POST": 488 | encoded_post_data = req.to_postdata() 489 | else: 490 | encoded_post_data = None 491 | url = req.to_url() 492 | else: 493 | url = self._BuildUrl(url, extra_params=extra_params) 494 | encoded_post_data = self._EncodePostData(post_data) 495 | 496 | # Open and return the URL immediately if we're not going to cache 497 | if encoded_post_data or no_cache or not self._cache or not self._cache_timeout: 498 | response = opener.open(url, encoded_post_data) 499 | url_data = self._DecompressGzippedResponse(response) 500 | opener.close() 501 | else: 502 | # Unique keys are a combination of the url and the oAuth Consumer Key 503 | if self._consumer_key: 504 | key = self._consumer_key + ':' + url 505 | else: 506 | key = url 507 | 508 | #TODO I turn off the cache as it bugged all the app, 509 | #but I need to see what's wrong with that. 510 | # See if it has been cached before 511 | #last_cached = self._cache.GetCachedTime(key) 512 | 513 | # If the cached version is outdated then fetch another and store it 514 | #if not last_cached or time.time() >= last_cached + self._cache_timeout: 515 | try: 516 | response = opener.open(url, encoded_post_data) 517 | url_data = self._DecompressGzippedResponse(response) 518 | #self._cache.Set(key, url_data) 519 | except urllib2.HTTPError, e: 520 | print e 521 | opener.close() 522 | #else: 523 | #url_data = self._cache.Get(key) 524 | 525 | # Always return the latest version 526 | return url_data 527 | 528 | def PostRetweet(self, id): 529 | '''This code come from issue #130 on python-twitter tracker''' 530 | 531 | if not self._oauth_consumer: 532 | raise TwitterError("The twitter.Api instance must be authenticated.") 533 | try: 534 | if int(id) <= 0: 535 | raise TwitterError("'id' must be a positive number") 536 | except ValueError: 537 | raise TwitterError("'id' must be an integer") 538 | url = 'http://api.twitter.com/1/statuses/retweet/%s.json' % id 539 | json_data = self._FetchUrl(url, post_data={'dummy': None}) 540 | data = json.loads(json_data) 541 | self._CheckForTwitterError(data) 542 | return Status.NewFromJsonDict(data) 543 | 544 | def GetCachedTime(self,key): 545 | path = self._GetPath(key) 546 | if os.path.exists(path): 547 | return os.path.getmtime(path) 548 | else: 549 | return None 550 | 551 | def SetCache(self, cache): 552 | '''Override the default cache. Set to None to prevent caching. 553 | 554 | Args: 555 | cache: 556 | An instance that supports the same API as the twitter._FileCache 557 | ''' 558 | if cache == DEFAULT_CACHE: 559 | self._cache = _FileCache() 560 | else: 561 | self._cache = cache 562 | 563 | def GetListStatuses(self,user,slug, per_page=None,page=None,since_id=None,max_id=None): 564 | '''Fetch the List statuses for a given user / list. 565 | 566 | Args: 567 | user: the username or id of the user whose list you are fetching. 568 | slug: slug of the list to fetch 569 | since_id: return only statuses with an ID greater than the specified ID 570 | [optional] 571 | max_id: return only statuses with an ID less than or equal to the 572 | specified ID [optional] 573 | per_page: specifies the maximum number of statuses to retrieve. Must be 574 | <= 200 [optional] 575 | page: specifies the page to retrieve [optional] 576 | ''' 577 | url = self.base_url 578 | path_elements = ['lists','statuses.json'] 579 | params = {'slug':slug, 580 | 'owner_screen_name':user, 581 | 'include_entities':'true'} 582 | if since_id: 583 | params['since_id']=since_id 584 | if max_id: 585 | params['max_id']=max_id 586 | if page: 587 | params['page']=page 588 | if per_page: 589 | params['per_page'] = per_page 590 | url = self._BuildUrl(url,path_elements,params) 591 | json_data = self._FetchUrl(url) 592 | data = json.loads(json_data) 593 | self._CheckForTwitterError(data) 594 | return [Status.NewFromJsonDict(x) for x in data] 595 | -------------------------------------------------------------------------------- /doc/reference.txt: -------------------------------------------------------------------------------- 1 | Tyrs Reference Guide 2 | ==================== 3 | :author: Nicolas Paris 4 | 5 | [CAUTION] 6 | The user interface of Tyrs is currently under eavy changements, some 7 | configurations who's interact with the interface isn't yet re-implements, and 8 | will be in a near release. 9 | 10 | [[installation]] 11 | Installation 12 | ------------ 13 | 14 | Easy way 15 | ~~~~~~~~ 16 | 17 | Tyrs is now available from pypi.python.org site, allowing you to use the 18 | easy_install method, and quickly get it started. If your using python2.7, all you need to do is: 19 | 20 | [source,bash] 21 | sudo easy_install http://pypi.python.org/packages/2.7/t/tyrs/tyrs-0.5.0-py2.7.egg 22 | 23 | Or you may visit the http://pypi.python.org/pypi/tyrs and get the latest tarball 24 | and proceed like so: 25 | 26 | [source, bash] 27 | tar xvf tyrs-0.3.2.tar.gz && cd tyrs 28 | python setup.py build 29 | python setup.py install 30 | 31 | You may also use python-pip tool to easily install tyrs: 32 | [source,bash] 33 | sudo pip install tyrs 34 | 35 | ArchLinux 36 | ~~~~~~~~~ 37 | 38 | As Tyrs is available in AUR, this installation is the most straightforward. 39 | Just use an AUR helper such as 'Yaourt', and grab a copy from the user 40 | repository: 41 | 42 | [source,bash] 43 | yaourt -S tyrs 44 | 45 | Ubuntu 46 | ~~~~~~ 47 | 48 | [source,bash] 49 | apt-get install python-setuptools python-distutils-extra 50 | wget http://pypi.python.org/packages/source/t/tyrs/tyrs-0.5.0.tar.gz 51 | tar xvf tyrs-0.5.0.tar.gz && cd tyrs 52 | python setup.py build 53 | sudo easy_install dist/tyrs-0.5.0-py2.7.egg 54 | 55 | Ubuntu users may also use the `easy way` approach above. 56 | 57 | Debian 58 | ~~~~~~ 59 | 60 | It seems that Tyrs works only with python2.7. You will need to use wheezy 61 | instead of squeezy. If the .egg from pypi does not work, here is how to install manually. 62 | 63 | Download and untar the latest Tyrs version. 64 | There are some issues with SSL and the 0.7.0 version of python-httplib2, so you 65 | may need to install the 0.6.0 version like: 66 | [source,bash] 67 | easy_install-2.7 http://httplib2.googlecode.com/files/httplib2-0.6.0.zip 68 | 69 | After we generate the .egg with: 70 | [source,bash] 71 | python2.7 setup.py bdist_egg 72 | 73 | And install it: 74 | easy_install-2.7 dist/tyrs-{revision}-py2.7.egg 75 | 76 | 77 | 78 | 79 | Frugalware 80 | ~~~~~~~~~~ 81 | Tyrs is now available on 82 | link:http://frugalware.org/packages/119439[Frugalware], thanks to Devil505 83 | 84 | [source,bash] 85 | pacman -S tyrs 86 | 87 | Gentoo 88 | ~~~~~~ 89 | 90 | You may install Tyrs on gentoo with the following 91 | link:https://github.com/linkdd/linkdd-overlay/blob/master/net-misc/tyrs/tyrs-9999.ebuild[ebuild on github], 92 | you may check the link:https://github.com/linkdd/linkdd-overlay/blob/master/README[README] 93 | for more informations, and how to add the overlay with 'layman'. Thanks to linkdd. 94 | 95 | OpenSuse 96 | ~~~~~~~~ 97 | 98 | I'm not very familiar with opensuse, but it seems available with: 99 | http://r.opensu.se/network:utilities.repo 100 | 101 | From source 102 | ~~~~~~~~~~~ 103 | 104 | Dependencies 105 | ^^^^^^^^^^^^ 106 | 107 | Tyrs requires the following dependencies 108 | 109 | * Python 2 110 | * link:http://pypi.python.org/pypi/setuptools[python-setuptools] or 111 | link:http://pypi.python.org/pypi/distribute[python-distribute] to use the 112 | setup.py install 113 | 114 | Those dependencies are installed automatically with the setup.py script. 115 | * link:http://code.google.com/p/python-twitter/[python-twitter] 116 | * link:https://github.com/simplegeo/python-oauth2[python-oauth2] 117 | 118 | Dependencies should be installed when you use the setup.py tool, but you may want 119 | to install them yourself. Note that you need a recent python-twitter package to use the 120 | oauth system. 121 | 122 | Installation 123 | ^^^^^^^^^^^^ 124 | 125 | Once you have the package and dependencies, there are two ways to get Tyrs 126 | installed: 127 | 128 | * With _setup.py_, this allows a clean install, similar to the autotools for C 129 | programs (`configure', `make', `make install') 130 | 131 | First, get the latest tarball (curently, the latest one is the {revision}) + 132 | http://src.nicosphere.net/tyrs/tyrs-{revision}.tar.gz 133 | 134 | [source,bash] 135 | tar xvf tyrs-0.3.1.tar.gz 136 | cd tyrs 137 | 138 | .With setup.py 139 | 140 | python setup.py build 141 | sudo python setup.py install 142 | 143 | Those commands decompress the tarball, go to the directory, prepare the 144 | installation (you can check the files that will be installed here), and then 145 | install it. Note that _the last command must be issued as_ *root*. And that's 146 | all, just run Tyrs with a simple: 147 | 148 | [source,bash] 149 | tyrs 150 | 151 | Github 152 | ~~~~~~ 153 | 154 | The development of Tyrs is based at link:https://github.com/Nic0/tyrs[Github], 155 | you can easily get the latest development version. 156 | 157 | [source,bash] 158 | git clone git://github.com/Nic0/tyrs.git 159 | 160 | You may follow the same installation steps seen above to install and run from 161 | the git repository. 162 | 163 | .Arch Linux 164 | [TIP] 165 | ================================= 166 | A package is available from AUR to retrieve the latest verion from github. 167 | [source,bash] 168 | yaourt -S tyrs-git 169 | 170 | ================================= 171 | 172 | Usage 173 | ----- 174 | 175 | Please check the configuration section for a list of keybindings. 176 | 177 | Activities bar 178 | ~~~~~~~~~~~~~~ 179 | 180 | The activities bar, is the bar containing information about your messages (the 181 | 'H:0 M:0 D:0 SUFTR' at the top), this displays new tweets in other buffers. For 182 | example, if you have new mentions in your timeline, the 'M' counter will 183 | increase. 184 | 185 | The meaning of each timelines is: 186 | 187 | * H: Home 188 | * M: Mentions 189 | * D: Direct messages 190 | * S: Search results 191 | * U: Users public timeline 192 | * F: Favorite tweets 193 | * T: Threads 194 | * R: own retweets 195 | 196 | A few things to note: 197 | 198 | * The current buffer is always set to zero, as you're actually reading it, there 199 | is no need to increase it. 200 | * On startup, other buffers are delayed initially, so they won't display new 201 | counts until after the first update. 202 | 203 | Icons 204 | ~~~~~ 205 | 206 | * ♻ Recycling icon, is a symbol to recognize retweets, the nick right after this 207 | symbol is the person who retweeted it. 208 | * ✉ Envelope icon, notify that the tweet is a response to another one, you may 209 | read the whole thread, with the thread shortcut 'T'. 210 | 211 | Url Shorter 212 | ~~~~~~~~~~~ 213 | 214 | You can shorter an URL automatically when you are editing a tweet. You have 215 | first to write the url, and then press 'Ctrl-u' to shorter it. The default 216 | behavior is to use the ur1.ca service, wich is an open-source project, but you 217 | may change this, read the params section for full details. 218 | 219 | Several shortener service are available: 220 | 221 | * ur1.ca 222 | * bit.ly 223 | * goo.gl 224 | * msud.pl 225 | 226 | Log file 227 | ~~~~~~~~ 228 | 229 | A log file is stored in the '~/.config/tyrs/tyrs.log' for debugging purpose, 230 | the verbosity may be increase by changing the logging_level params to 1 in the 231 | configuration file. It might be useful to understing if somethings wrong or for 232 | bugs repports 233 | 234 | Console arguments 235 | ----------------- 236 | 237 | There is more information on these commands in each section (configuration and 238 | installation). 239 | 240 | * '-a', or '--account': source another account 241 | * '-c', or '--config': source another config file 242 | * '-g', or '--generate-config': generate a default configuration file. 243 | * '-h', or '--help': help 244 | 245 | [[configuration]] 246 | Configuration 247 | ------------- 248 | 249 | Introduction 250 | ~~~~~~~~~~~~ 251 | 252 | Tyrs is quite customizable. 253 | 254 | It's based on the 'ConfigParser' core module from python, and it's a very 255 | readable and to understand the syntax. All the configuration is based on this 256 | schema. 257 | 258 | [source,conf] 259 | ------------ 260 | [section1] 261 | param1 = string 262 | param2 = 42 263 | [section2] 264 | other_param = otherValue 265 | ... 266 | ----------------- 267 | A quick way to get started with configuration is to generate a config file 268 | from the command line. with the '-g' option (or '--generate-config'), this 269 | parameter takes one argument: the filename. 270 | 271 | [source,bash] 272 | tyrs -g ~/.config/tyrs/tyrs.cfg 273 | 274 | There are four sections for configuration: 275 | 276 | * *colors*: Customization of colors. 277 | * *keybinding*: Customization of key shortcuts. 278 | * *params*: Other parameters. 279 | * *filter*: Filter tweets without URL. 280 | 281 | [NOTE] 282 | ====================== 283 | * All sections and parameters are lower case. For params composed with two 284 | words, they are separate with an underscore `'_'' (no camelCase). 285 | * A default configuration file is generated if you don't have one yet on 286 | the first start. 287 | ====================== 288 | 289 | Configuration file 290 | ~~~~~~~~~~~~~~~~~~ 291 | 292 | The configuration file is located at '~/.config/tyrs/tyrs.cfg'. 293 | You may have several configuration files, or accounts, 294 | passed as an argument when you start Tyrs. The argument 295 | is appended to the filename of the configuration. 296 | 297 | .Custom config file 298 | [source,bash] 299 | $ tyrs -c foo 300 | 301 | This will read the configuration from the '~/.config/tyrs/tyrs.cfg.foo'. 302 | 303 | CAUTION: You don't need to pass the full filename, as it will attempt to read 304 | the file from 'tyrs.cfg.tyrs.cfg.foo' file if you do so. 305 | 306 | TIP: You may add comments to your configuration file, you just need to 307 | start the line with '#' or ';'. 308 | 309 | Colors 310 | ~~~~~~ 311 | 312 | Values possible 313 | ^^^^^^^^^^^^^^^ 314 | 315 | The possibles values are determined by curses mode. The available 316 | values are: 317 | 318 | * black 319 | * dark red 320 | * dark green 321 | * brown 322 | * dark blue 323 | * dark magenta 324 | * dark cyan 325 | * light gray 326 | * dark gray 327 | * light red 328 | * light green 329 | * yellow 330 | * light blue 331 | * light magenta 332 | * light cyan 333 | * white 334 | 335 | Overview 336 | ^^^^^^^^ 337 | 338 | These are the default values: 339 | 340 | [source,conf] 341 | ---------------- 342 | [colors] 343 | body = dark blue 344 | focus = dark red 345 | header = light blue 346 | line = light blue 347 | info_msg = dark green 348 | warn_msg = dark red 349 | current_tab = light blue 350 | other_tab = dark blue 351 | read = dark blue 352 | unread = dark red 353 | hashtag = dark green 354 | attag = brown 355 | highlight = dark red 356 | highlight_nick = light red 357 | help_bar = yellow 358 | help_key = dark red 359 | ---------------- 360 | 361 | Bold font 362 | ^^^^^^^^^ 363 | 364 | Bold font usualy correspond to the *light* version of a color, *dark* 365 | correspond to the normal font. 366 | 367 | 368 | Listing 369 | ^^^^^^^ 370 | 371 | highlight:: 372 | * Description: This will highlight when your nick is display in a tweet, 373 | for example, a tweet « blabla @you blabla », @you will be color highlighted. 374 | 375 | header:: 376 | * Description: Corresponding to the display information above the tweet 377 | such as user name of the tweeter, time, and any retweet information. 378 | 379 | line:: 380 | * Description: Corresponding to the border of a tweet. 381 | 382 | current_tweet:: 383 | * Description: Corresponding to the header color of the current (selected) 384 | tweet. 385 | 386 | current_tab:: 387 | * Description: Display the current timeline (home, mentions or direct message) 388 | in highlighted color. 389 | 390 | other_tab:: 391 | * Description: Color of any other than the current buffer, in the activities bar. 392 | 393 | hashtag:: 394 | * Description: This will highlight the hashtag `#` word, useful to follow 395 | specific subjects, like _#Tyrs_. 396 | 397 | attag:: 398 | * Description: This will highlight when a name is followed by a `@' tag like 399 | _@nic0sphere_. 400 | 401 | warning_msg:: 402 | * Description: When something goes wrong, or unexpected, a warning message is 403 | displayed on the top left of the console. 404 | * Examples: `Couldn\'t retrieve tweets', `Couldn\'t send tweet'... 405 | 406 | info_msg:: 407 | * Description: When an action is successful or other feedback is required, Tyrs 408 | lets you know. Similar to warning_msg. 409 | * Examples: `Updating timeline', `Tweet send successfully'... 410 | 411 | text:: 412 | * Description: This is all the basic text, such as tweets (without highlight), 413 | or input in the tweet edit box. 414 | 415 | read:: 416 | * Description: Color in the activities bar when its set to 0. 417 | 418 | unread:: 419 | * Description: Color in the activities bar when you have unread tweets. 420 | 421 | help:: 422 | * Description: Color of the main help screen. 423 | 424 | 425 | 426 | Configuration 256 Colors 427 | ~~~~~~~~~~~~~~~~~~~~~~~~ 428 | 429 | [IMPORTANT] 430 | =================== 431 | The 256 Colors works only with version of Tyrs older than 6.0, it will be avialable soon. 432 | =================== 433 | 434 | Key Binding 435 | ~~~~~~~~~~~ 436 | 437 | Overview 438 | ^^^^^^^^ 439 | 440 | This allows you to change keys shortcuts to suit your needs. These are the defaults: 441 | 442 | [source,conf] 443 | -------------- 444 | [keys] 445 | up = k 446 | down = j 447 | left = J 448 | right = K 449 | back_to_top = g 450 | back_to_bottom = G 451 | quit = q 452 | tweet = t 453 | clear = c 454 | retweet = r 455 | retweet_and_edit = R 456 | update = u 457 | follow_selected = f 458 | unfollow_selected = l 459 | follow = F 460 | unfollow = L 461 | openurl = o 462 | home = h 463 | mentions = m 464 | reply = M 465 | getDM = d 466 | sendDM = D 467 | search = s 468 | redraw = ctrl l 469 | search_user = U 470 | search_current_user = ctrl f 471 | search_myself = ctrl u 472 | delete = C 473 | fav = b 474 | get_fav = B 475 | delete_fav = ctrl b 476 | thread = T 477 | open_image = ctrl i 478 | waterline = w 479 | --------------- 480 | 481 | Control keys support 482 | ^^^^^^^^^^^^^^^^^^^^^ 483 | 484 | You may activate control keys (Ctrl+something) in the configuration file, for 485 | example, remap your reply with a Ctrl-r, works the same way with *alt* and *meta* touch: 486 | 487 | [source,conf] 488 | --------------- 489 | [keys] 490 | reply = ctrl r 491 | foo = alt r 492 | bar = meta r 493 | --------------- 494 | 495 | Listing 496 | ^^^^^^^ 497 | 498 | up:: 499 | * Description: Navigation — moves up in the timeline. 500 | * Alternative: Up arrow 501 | * Default: *k* 502 | 503 | down:: 504 | * Description: Navigation — moves down in the timeline. 505 | * Alternative: Down arrow 506 | * Default: *j* 507 | 508 | left:: 509 | * Description: Navigation — moves left in different timelines (home, mentions, 510 | direct, search) 511 | * Alternative: Left arrow 512 | * Default: *J* 513 | 514 | right:: 515 | * Description: Navigation — moves right in different timelines. 516 | * Alternative: Right arrow 517 | * Default: *K* 518 | 519 | back_to_top:: 520 | * Description: Navigation — moves on top in the timeline. 521 | * Default: *g* 522 | 523 | back_to_bottom:: 524 | * Description: Navigation — moves on bottom of the screen. 525 | * Default: *G* 526 | 527 | quit:: 528 | * Description: Leave Tyrs, and return to a normal life. 529 | * Default: *q* 530 | 531 | tweet:: 532 | * Description: Open an input box to write your tweet. 533 | * Default: *t* 534 | 535 | clear:: 536 | * Description: Clear the timeline and leave only the last tweet. 537 | * Default: *c* 538 | 539 | retweet:: 540 | * Description: Retweet the current tweet. 541 | * Default: *r* 542 | 543 | retweet_and_edit:: 544 | * Description: This allows you to add some comment to a retweet, it will 545 | display as: "RT @user_name: his_tweet". 546 | * Default: *R* 547 | 548 | [NOTE] 549 | This kind of retweet is not realy a retweet, its more like a normal tweet but, 550 | with the content of the original tweet and the tweeter's username added. 551 | 552 | home:: 553 | * Description: Go back to your home timeline. 554 | * Default: *h* 555 | 556 | mentions:: 557 | * Description: Shows you all tweets that contain your nick (@you), meaning if someone 558 | that you are not following puts your @name in a tweet, you will see it. You may 559 | leave this timeline using 'h' (home) to go back on your home timeline. 560 | * Default: *m* 561 | 562 | reply:: 563 | * Description: Reply to the current tweet, open an edit box, with the name of 564 | the current tweet preloaded. It will be show as a response to the current 565 | selected tweet. 566 | * Defaurt *M* 567 | 568 | update:: 569 | * Description: Force the update of your timeline. 570 | * Default: *u* 571 | 572 | follow_selected:: 573 | * Description: Follow the currently highlighted tweeter. 574 | * Default: *f* 575 | 576 | unfollow_selected:: 577 | * Description: Unfollow (leave) the person who tweets (or retweets) the current 578 | tweet. 579 | * Default: *l* 580 | 581 | follow:: 582 | * Description: Follow someone, but the difference with `follow_selected' is 583 | that you will have a small input box, and can enter someone's nick to follow 584 | them. 585 | * Default: *F* 586 | 587 | unfollow:: 588 | * Description: Unfollow someone, display an input box to enter their nick. 589 | * Default: *L* 590 | 591 | [NOTE] 592 | You may enter the name to follow with or without the '@', so it's quicker to do 593 | without. 594 | 595 | open profile:: 596 | * Description: print the profile of the currently selected tweeter. 597 | * Default: *i* 598 | 599 | openurl:: 600 | * Description: will open the link in the selected tweet. 601 | * Default: *o* 602 | 603 | getDM:: 604 | * Description: Retrieve your direct messages 605 | * Default: *d* 606 | 607 | sendDM:: 608 | * Description: Send a direct message, Tyrs will prompt you to send a direct 609 | message to the current tweeter, but you may change (or simply check) the user 610 | before writing your message. 611 | * Default: *D* 612 | 613 | search:: 614 | * Description: Return a list of tweets that match your search, 615 | a box will appear to let you enter a string to search for 616 | * Default: *s* 617 | 618 | search_user:: 619 | * Description: Retrieve the specified user's timeline (usually useful to review 620 | someone's tweets before deciding to follow them or not). 621 | * Default: *U* 622 | 623 | search_user:: 624 | * Description: Retrieve the current twitter's timeline (usually useful to review 625 | someone's tweets before deciding to follow them or not). 626 | * Default: *^F* 627 | 628 | search_myself:: 629 | * Description: Retrieve your own timeline (useful to delete a tweet). 630 | * Default: *^U* 631 | 632 | delete:: 633 | * Description: Destroy the selected tweet. 634 | * Default: *C* 635 | 636 | open avatar image:: 637 | * Description: open the avatar of the current selected tweeter in your image viewer. 638 | * Default: *^I* (ctrl-i) 639 | 640 | redraw:: 641 | * Description: Force Tyrs to be redrawn. 642 | * Default: *^L* (ctrl-l) 643 | 644 | fav:: 645 | * Description: Tag the current tweet as favorite. 646 | * Default: *b* 647 | 648 | get_fav:: 649 | * Description: Retrieve all your favorites in a timeline. 650 | * Default: *B* 651 | 652 | delete_fav:: 653 | * Description: Destroy the selected favorite. 654 | * Default: *^B* (ctrl-b) 655 | 656 | thread:: 657 | * Description: Prints the thread of mentions/responses for a tweet (with 658 | an envelope icon) 659 | * Default: *T* 660 | 661 | open_image:: 662 | * Description: Retreive the avatar of the current twitter. 663 | * Default: *^I* 664 | 665 | waterline:: 666 | * Description: Allow to set the waterline, the doted line, at the latest read 667 | tweet. 668 | * Default: *w* 669 | 670 | [TIP] 671 | A good way to remember the favorites shortcut, think of them as your *b*est tweets. 672 | 673 | 674 | [CAUTION] 675 | Commands do not ask for comfirmation. Be careful when using commands such as 676 | delete or unfollow. 677 | 678 | Parameters 679 | ~~~~~~~~~~ 680 | 681 | Overview 682 | ^^^^^^^^ 683 | 684 | [source,conf] 685 | ------------------- 686 | [params] 687 | refresh = 3 688 | tweet_border = 1 689 | relative_time = 0 690 | retweet_by = 1 691 | openurl_command = firefox %s 692 | open_image_command = feh %s 693 | transparency = 1 694 | activities = 1 695 | help = 1 696 | margin = 1 697 | padding = 2 698 | compress = 0 699 | source = 0 700 | url_shorter = ur1ca 701 | logging_level = 3 702 | header_template = (see listing) 703 | beep = 1 704 | ------------------- 705 | 706 | Listing 707 | ^^^^^^^ 708 | 709 | refresh:: 710 | * Description: Define the frequency that the timeline will be refreshed. 711 | * Values: Any positive value. 712 | * Unit: Minutes 713 | * Default: 3 714 | 715 | tweet_border:: 716 | * Description: Display a border around each tweet 717 | * Values: 0 or 1 718 | * Default: 1 719 | 720 | relative_time:: 721 | * Description: If set to 1, this will display the time of each tweet like `xx 722 | minutes ago', otherwise, will display a more classic format like `HH:MM'. 723 | * Values: 0 or 1 724 | * Default: 0 725 | 726 | retweet_by:: 727 | * Description: Normally, if a tweet is a retweet the header will be: 728 | `tweeter (some time ago) RT by retweeter', if you find 729 | it too verbose, setting it to 0 will display only the retweeter. 730 | * Values: 0 or 1 731 | * Default: 1 732 | 733 | openurl_command:: 734 | * Description: Lets you choose your favorite browser for opening urls, note 735 | the %s is *required* to pass the corresponding url that will be opened. 736 | * Values: any web browser 737 | * Default: firefox %s 738 | 739 | open_image_command:: 740 | * Description: Lets you choose your favorite image viewer. Note, the %s is 741 | *required*. 742 | * Values: any command that opens an image. 743 | * Default: feh %s 744 | 745 | activities:: 746 | * Description: Disable the activities bar if set to 0. 747 | * Values: 0 or 1 748 | * Default: 1 749 | 750 | help:: 751 | * Description: Display commonly used commands at the bottom of the screen, set 752 | to 0 to disable it. 753 | * Values: 0 or 1 754 | * Default: 1 755 | 756 | [TIP] 757 | If you are not familiar with margin and padding, it's taken from the CSS vocabulary. 758 | 759 | margin:: 760 | * Description: Space between the border of a tweet, and the edge of the terminal 761 | * Default: 1 762 | 763 | padding:: 764 | * Description: Space between the border of a tweet, and the start on the text 765 | himself. 766 | * Default: 2 767 | 768 | compress:: 769 | * Description: disable the blank line between tweets. 770 | * Values 0, 1 771 | * Default: 0 772 | 773 | url_shorter:: 774 | * Description: Change the URL shortening service. 775 | * Values: ur1ca, bitly, googl, msudpl 776 | * Default: ur1ca 777 | 778 | logging_level:: 779 | ** Description: Increase informations stored in the log file, 1 is the most 780 | verbose, 5 is the less. 781 | * Values: 1 to 5 782 | * Default: 3 783 | 784 | header_template:: 785 | ** Description: Let you chose the how the header of each tweet will be 786 | displayed, following a template code. 787 | * Values: string containing specials parameters with {} 788 | * Default: 789 | 790 | beep:: 791 | ** Description: Do a console beep when receiving unread updates. 792 | * Values: boolean, 0 or 1 793 | * Default: 0 794 | 795 | [source, conf] 796 | ----------------- 797 | {time} - {nick}{retweeted}{retweeter}{reply}{retweet_count} 798 | ----------------- 799 | 800 | With: 801 | 802 | * time: hour, or 'time ago' 803 | * nick: the twitter nick 804 | * retweeted: the retweet icon, if needed. 805 | * retweeter: the person who retweet. 806 | * source: append tweeter's client to the header of a tweet (eg,. Tyrs, TTytter etc) 807 | * reply: envelope icon, if the tweet is a reply to another one. 808 | * retweet_count: the count of retweet for this tweet. 809 | 810 | [NOTE] 811 | ============================= 812 | If you are using a small terminal, and don't want waste any spaces, you may try 813 | a configuration like: 814 | [source, conf] 815 | ----------------- 816 | [params] 817 | tweet_border = 0 818 | compress = 1 819 | margin = 0 820 | padding = 0 821 | ------------------ 822 | ============================= 823 | 824 | Filter 825 | ~~~~~~ 826 | 827 | This option allows you to have more control over tweets displayed in your 828 | timeline. It filters tweets without a URL, leaving only those with links. There 829 | are a few options to allow you to adjust the filter as you wish. 830 | 831 | 832 | Overview 833 | ^^^^^^^^ 834 | 835 | [source,conf] 836 | ------------------- 837 | [filter] 838 | activate = 1 839 | behavior = all 840 | myself = 0 841 | except = nick1 nick2 842 | ------------------- 843 | 844 | Listing 845 | ^^^^^^^ 846 | 847 | activate:: 848 | * Description: Activate the filter system if set to 1 849 | * Values: 0, 1 850 | * Default: 0 851 | 852 | behavior:: 853 | * Description: This option sets the behavior of the filter and exception. If set to 854 | all, all tweets that don't contain a URL will be filtered except 855 | the list passed in the except params. If set to none, it will print all 856 | tweets, but you can still add some exceptions that will be filtered. 857 | * Values: all, none 858 | * Default: all 859 | 860 | myself:: 861 | * Description: If a tweet doesn't contain a URL, but has your nick in 862 | it, you may want read it. This option disables the filter on those tweets. Default value 863 | allow to read them, set to 1 if you want to filter them. 864 | * Values: 0, 1 865 | * Default: 0 866 | 867 | except:: 868 | * Description: It depends on the value of 'behavior.' If you have set it to a 869 | restrictive value (all), except allows you to set some exceptions and read 870 | tweets without URL from a list of nicks. If you set it to none, Tyrs will print 871 | all tweets without a URL, but will only filter names that 872 | you add to this list. Useful to just filter few nicks. 873 | * Values: List of nicks 874 | * Default: Empty list. 875 | --------------------------------------------------------------------------------