Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

merge conflicts solved

  • Loading branch information...
commit c2a072edc716fa34b74f850904ac1f356daba982 2 parents ce704c1 + 82e17c1
Alejandro Gómez authored
View
12 HISTORY.rst
@@ -1,3 +1,15 @@
+0.1.4
+-----
+- update all timelines periodically
+- configurable default timelines
+- bugfix: don't crash with empty timelines
+- bugfix: manual retweet crashed
+- bugfix: don't capture all input
+
+0.1.3
+-----
+- bugfix: packaging error
+
0.1.2
-----
- bugfix: error with packaging
View
3  MANIFEST
@@ -1,9 +1,8 @@
setup.py
bin/turses
turses/__init__.py
-turses/cli.py
turses/config.py
-turses/controller.py
+turses/core.py
turses/models.py
turses/ui.py
turses/utils.py
View
4 Makefile
@@ -1,5 +1,5 @@
APPNAME=turses
-VERSION=0.1.3
+VERSION=0.1.4
DISTPKG=dist/$(APPNAME)-$(VERSION).tar.gz
PY=python
@@ -8,7 +8,7 @@ PIPI=pip install
PIPFLAGS=--ignore-installed --no-deps
TESTRUNNER=nosetests
-TESTFLAGS=--with-color --nocapture --logging-clear-handlers --with-coverage --cover-package=turses
+TESTFLAGS=--nocapture --logging-clear-handlers --with-coverage --cover-package=turses
WATCHTESTFLAGS=--verbosity=0
View
26 bin/turses
@@ -1,5 +1,27 @@
#!/usr/bin/python
-from turses.cli import main
-main()
+from urwid import set_encoding
+
+from turses.utils import parse_arguments
+from turses.config import Configuration
+from turses.ui import CursesInterface
+from turses.api.backends import TweepyApi
+from turses.core import Turses
+
+
+try:
+ set_encoding('utf8')
+
+ args = parse_arguments()
+
+ configuration = Configuration(args)
+ configuration.load()
+ ui = CursesInterface(configuration)
+
+ # start `turses`
+ Turses(configuration=configuration,
+ ui=ui,
+ api_backend=TweepyApi)
+except KeyboardInterrupt:
+ exit(0)
View
19 tests/test_models.py
@@ -307,6 +307,25 @@ def test_clear(self):
self.timeline.add_statuses([old_status, new_status])
self.assertEqual(len(self.timeline), 2)
+ def test_get_unread_count(self):
+ self.assertEqual(self.timeline.get_unread_count(), 0)
+
+ # a status
+ status = create_status(id=1)
+ self.timeline.add_status(status)
+ self.assertEqual(self.timeline.get_unread_count(), 1)
+
+ self.timeline.mark_all_as_read()
+ self.assertEqual(self.timeline.get_unread_count(), 0)
+
+ # new statuses
+ statuses = [create_status(id=id_num) for id_num in xrange(2, 10)]
+ self.timeline.add_statuses(statuses)
+ self.assertEqual(self.timeline.get_unread_count(), len(statuses))
+
+ self.timeline.mark_all_as_read()
+ self.assertEqual(self.timeline.get_unread_count(), 0)
+
# update function related
def test_extract_with_no_args(self):
View
2  turses/__init__.py
@@ -13,6 +13,6 @@
__author__ = "Alejandro Gómez"
__copyright__ = "Copyright 2012 turses contributors"
__license__ = "GPL3"
-__version__ = (0, 1, 3)
+__version__ = (0, 1, 4)
version = "%s.%s.%s" % __version__
View
33 turses/cli.py
@@ -1,33 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""
-turses.cli
-~~~~~~~~~~
-
-This module contains the logic to launch `turses` with a curses interface.
-"""
-
-from urwid import set_encoding
-
-from .utils import parse_arguments
-from .config import Configuration
-from .controller import CursesController
-from .ui import CursesInterface
-from .api.backends import TweepyApi
-
-
-def main():
- try:
- set_encoding('utf8')
-
- args = parse_arguments()
- configuration = Configuration(args)
- configuration.load()
- ui = CursesInterface(configuration)
-
- # start `turses`
- CursesController(configuration=configuration,
- ui=ui,
- api_backend=TweepyApi)
- except KeyboardInterrupt:
- exit(0)
View
234 turses/config.py
@@ -38,6 +38,7 @@
`
"""
+from sys import exit
from ConfigParser import RawConfigParser
from os import getenv, path, mkdir, remove
from gettext import gettext as _
@@ -47,6 +48,24 @@
# -- Defaults -----------------------------------------------------------------
+# Timelines
+
+HOME_TIMELINE = 'home'
+MENTIONS_TIMELINE = 'mentions'
+FAVORITES_TIMELINE = 'favorites'
+MESSAGES_TIMELINE = 'messages'
+OWN_TWEETS_TIMELINE = 'own_tweets'
+
+DEFAULT_TIMELINES = {
+ HOME_TIMELINE: True,
+ MENTIONS_TIMELINE: True,
+ FAVORITES_TIMELINE: True,
+ MESSAGES_TIMELINE: True,
+ OWN_TWEETS_TIMELINE: True,
+}
+
+# Key bindings
+
KEY_BINDINGS = {
# motion
'up':
@@ -221,6 +240,8 @@
'redraw',
]
+# Palette
+
# TODO: not hard coded
# valid colors for `urwid`s palette
VALID_COLORS = [
@@ -275,6 +296,8 @@ def validate_color(colorstring):
['editor', 'white', 'dark blue'],
]
+# Styles
+
STYLES = {
# TODO: make time string configurable
'header_template': ' {username}{retweeted}{retweeter} - {time}{reply} {retweet_count} ',
@@ -282,9 +305,15 @@ def validate_color(colorstring):
}
# Debug
+
LOGGING_LEVEL = 3
+# Twitter
+
+UPDATE_FREQUENCY = 300
+
# Environment
+
HOME = getenv('HOME')
BROWSER = getenv('BROWSER')
@@ -302,10 +331,14 @@ def validate_color(colorstring):
LEGACY_TOKEN_FILE = path.join(LEGACY_CONFIG_PATH, 'turses.tok')
# Names of the sections in the configuration
+SECTION_DEFAULT_TIMELINES = 'timelines'
SECTION_KEY_BINDINGS = 'bindings'
SECTION_PALETTE = 'colors'
SECTION_STYLES = 'styles'
SECTION_DEBUG = 'debug'
+SECTION_TWITTER = 'twitter'
+
+# Names of the sections in the token file
SECTION_TOKEN = 'token'
@@ -366,6 +399,8 @@ def load(self):
def load_defaults(self):
"""Load default values into configuration."""
+ self.default_timelines = DEFAULT_TIMELINES
+ self.update_frequency = UPDATE_FREQUENCY
self.key_bindings = KEY_BINDINGS
self.palette = PALETTE
self.styles = STYLES
@@ -376,12 +411,86 @@ def _init_config(self):
self._parse_legacy_config_file()
print_deprecation_notice()
remove(LEGACY_CONFIG_FILE)
- self.generate_config_file(self.config_file)
elif path.isfile(self.config_file):
self.parse_config_file(self.config_file)
else:
self.generate_config_file(self.config_file)
+ def _add_section_default_timelines(self, conf):
+ # Default timelines
+ if not conf.has_section(SECTION_DEFAULT_TIMELINES):
+ conf.add_section(SECTION_DEFAULT_TIMELINES)
+ for timeline in DEFAULT_TIMELINES:
+ if conf.has_option(SECTION_DEFAULT_TIMELINES, timeline):
+ continue
+ value = str(self.default_timelines[timeline]).lower()
+ conf.set(SECTION_DEFAULT_TIMELINES, timeline, value)
+
+ def _add_section_twitter(self, conf):
+ # Twitter
+ if not conf.has_section(SECTION_TWITTER):
+ conf.add_section(SECTION_TWITTER)
+ if conf.has_option(SECTION_TWITTER, 'update_frequency'):
+ return
+ else:
+ conf.set(SECTION_TWITTER, 'update_frequency', UPDATE_FREQUENCY)
+
+ def _add_section_key_bindings(self, conf):
+ # Key bindings
+ if not conf.has_section(SECTION_KEY_BINDINGS):
+ conf.add_section(SECTION_KEY_BINDINGS)
+ binding_lists = [MOTION_KEY_BINDINGS,
+ BUFFERS_KEY_BINDINGS,
+ TWEETS_KEY_BINDINGS,
+ TIMELINES_KEY_BINDINGS,
+ META_KEY_BINDINGS,
+ TURSES_KEY_BINDINGS,]
+ for binding_list in binding_lists:
+ for binding in binding_list:
+ key = self.key_bindings[binding][0]
+ if conf.has_option(SECTION_KEY_BINDINGS, binding):
+ continue
+ conf.set(SECTION_KEY_BINDINGS, binding, key)
+
+ def _add_section_palette(self, conf):
+ # Color
+ if not conf.has_section(SECTION_PALETTE):
+ conf.add_section(SECTION_PALETTE)
+ for label in PALETTE:
+ label_name, fg, bg = label[0], label[1], label[2]
+
+ # fg
+ if conf.has_option(SECTION_PALETTE, label_name) and \
+ validate_color(conf.get(SECTION_PALETTE, label_name)):
+ pass
+ else:
+ conf.set(SECTION_PALETTE, label_name, fg)
+
+ #bg
+ label_name_bg = label_name + '_bg'
+ if conf.has_option(SECTION_PALETTE, label_name_bg) and \
+ validate_color(conf.get(SECTION_PALETTE, label_name_bg)):
+ pass
+ else:
+ conf.set(SECTION_PALETTE, label_name_bg, bg)
+
+ def _add_section_styles(self, conf):
+ # Styles
+ if not conf.has_section(SECTION_STYLES):
+ conf.add_section(SECTION_STYLES)
+ for style in STYLES:
+ if conf.has_option(SECTION_STYLES, style):
+ continue
+ conf.set(SECTION_STYLES, style, self.styles[style])
+
+ def _add_section_debug(self, conf):
+ # Debug
+ if not conf.has_section(SECTION_DEBUG):
+ conf.add_section(SECTION_DEBUG)
+ if conf.has_option(SECTION_DEBUG, 'logging_level'):
+ return
+ conf.set(SECTION_DEBUG, 'logging_level', LOGGING_LEVEL)
+
def _init_token(self):
if path.isfile(LEGACY_TOKEN_FILE):
self.parse_token_file(LEGACY_TOKEN_FILE)
@@ -453,45 +562,28 @@ def _set_key_binding(self, binding, new_key):
self.key_bindings[binding] = new_key_binding
def generate_config_file(self, config_file):
- self._generate_config_file(config_file=config_file,
- on_error=self._config_generation_error,
- on_success=self._config_generation_success)
+ kwargs = {
+ 'config_file': config_file,
+ 'on_error': self._config_generation_error,
+ }
+
+ if not path.isfile(config_file):
+ kwargs.update({
+ 'on_success': self._config_generation_success
+ })
+
+ self._generate_config_file(**kwargs)
@wrap_exceptions
def _generate_config_file(self, config_file):
conf = RawConfigParser()
- self.config_file = config_file
-
- # Key bindings
- conf.add_section(SECTION_KEY_BINDINGS)
- binding_lists = [MOTION_KEY_BINDINGS,
- BUFFERS_KEY_BINDINGS,
- TWEETS_KEY_BINDINGS,
- TIMELINES_KEY_BINDINGS,
- META_KEY_BINDINGS,
- TURSES_KEY_BINDINGS,]
- for binding_list in binding_lists:
- for binding in binding_list:
- key = self.key_bindings[binding][0]
- conf.set(SECTION_KEY_BINDINGS, binding, key)
-
-
- # Color
- conf.add_section(SECTION_PALETTE)
- for label in self.palette:
- label_name, fg, bg = label[0], label[1], label[2]
- conf.set(SECTION_PALETTE, label_name, fg)
- conf.set(SECTION_PALETTE, label_name + '_bg', bg)
-
- # Styles
- conf.add_section(SECTION_STYLES)
- for style in self.styles:
- conf.set(SECTION_STYLES, style, self.styles[style])
-
- # Debug
- conf.add_section(SECTION_DEBUG)
- conf.set(SECTION_DEBUG, 'logging_level', LOGGING_LEVEL)
+ self._add_section_default_timelines(conf)
+ self._add_section_twitter(conf)
+ self._add_section_key_bindings(conf)
+ self._add_section_palette(conf)
+ self._add_section_styles(conf)
+ self._add_section_debug(conf)
with open(config_file, 'wb') as config:
conf.write(config)
@@ -521,47 +613,63 @@ def generate_token_file(self,
print encode(_('your account has been saved'))
def parse_config_file(self, config_file):
- self._conf = RawConfigParser()
- self._conf.read(config_file)
-
- self._parse_key_bindings()
- self._parse_palette()
- self._parse_styles()
- self._parse_debug()
-
- def _parse_key_bindings(self):
+ conf = RawConfigParser()
+ conf.read(config_file)
+
+ self._parse_default_timelines(conf)
+ self._parse_twitter(conf)
+ self._parse_key_bindings(conf)
+ self._parse_palette(conf)
+ self._parse_styles(conf)
+ self._parse_debug(conf)
+
+ def _parse_default_timelines(self, conf):
+ for timeline in self.default_timelines:
+ if conf.has_option(SECTION_DEFAULT_TIMELINES, timeline):
+ try:
+ value = conf.getboolean(SECTION_DEFAULT_TIMELINES,
+ timeline)
+ except ValueError:
+ continue
+ self.default_timelines[timeline] = value
+
+ def _parse_twitter(self, conf):
+ if conf.has_option(SECTION_TWITTER, 'update_frequency'):
+ self.update_frequency = conf.getint(SECTION_TWITTER, 'update_frequency')
+
+ def _parse_key_bindings(self, conf):
for binding in self.key_bindings:
- if self._conf.has_option(SECTION_KEY_BINDINGS, binding):
- custom_key = self._conf.get(SECTION_KEY_BINDINGS, binding)
+ if conf.has_option(SECTION_KEY_BINDINGS, binding):
+ custom_key = conf.get(SECTION_KEY_BINDINGS, binding)
self._set_key_binding(binding, custom_key)
- def _parse_palette(self):
+ def _parse_palette(self, conf):
# Color
for label in self.palette:
label_name, fg, bg = label[0], label[1], label[2]
- if self._conf.has_option(SECTION_PALETTE, label_name):
- fg = self._conf.get(SECTION_PALETTE, label_name)
- if self._conf.has_option(SECTION_PALETTE, label_name + '_bg'):
- bg = self._conf.get(SECTION_PALETTE, label_name + '_bg')
+ if conf.has_option(SECTION_PALETTE, label_name):
+ fg = conf.get(SECTION_PALETTE, label_name)
+ if conf.has_option(SECTION_PALETTE, label_name + '_bg'):
+ bg = conf.get(SECTION_PALETTE, label_name + '_bg')
self._set_color(label_name, fg, bg)
- def _parse_styles(self):
+ def _parse_styles(self, conf):
for style in self.styles:
- if self._conf.has_option(SECTION_STYLES, style):
- self.styles[style] = self._conf.get(SECTION_STYLES, style)
+ if conf.has_option(SECTION_STYLES, style):
+ self.styles[style] = conf.get(SECTION_STYLES, style)
- def _parse_debug(self):
- if self._conf.has_option(SECTION_DEBUG, 'logging_level'):
- self.logging_level = self._conf.get(SECTION_DEBUG, 'logging_level')
+ def _parse_debug(self, conf):
+ if conf.has_option(SECTION_DEBUG, 'logging_level'):
+ self.logging_level = conf.get(SECTION_DEBUG, 'logging_level')
def parse_token_file(self, token_file):
- self._conf = RawConfigParser()
- self._conf.read(token_file)
+ conf = RawConfigParser()
+ conf.read(token_file)
- if self._conf.has_option(SECTION_TOKEN, 'oauth_token'):
- self.oauth_token = self._conf.get(SECTION_TOKEN, 'oauth_token')
- if self._conf.has_option(SECTION_TOKEN, 'oauth_token_secret'):
- self.oauth_token_secret = self._conf.get(SECTION_TOKEN, 'oauth_token_secret')
+ if conf.has_option(SECTION_TOKEN, 'oauth_token'):
+ self.oauth_token = conf.get(SECTION_TOKEN, 'oauth_token')
+ if conf.has_option(SECTION_TOKEN, 'oauth_token_secret'):
+ self.oauth_token_secret = conf.get(SECTION_TOKEN, 'oauth_token_secret')
def authorize_new_account(self):
access_token = authorization()
View
228 turses/controller.py → turses/core.py
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
"""
-turses.controller
-~~~~~~~~~~~~~~~~~
+turses.core
+~~~~~~~~~~~
This module contains the controller logic of turses.
"""
@@ -12,9 +12,17 @@
from functools import partial
import urwid
+from tweepy import TweepError
from .api.base import AsyncApi
from .utils import get_urls, spawn_process, wrap_exceptions, async
+from .config import (
+ HOME_TIMELINE,
+ MENTIONS_TIMELINE,
+ FAVORITES_TIMELINE,
+ MESSAGES_TIMELINE,
+ OWN_TWEETS_TIMELINE,
+)
from .models import (
is_DM,
is_username,
@@ -34,7 +42,7 @@
class KeyHandler(object):
"""
- Maps actions from configuration to calls to controllers' functions.
+ Maps key bindings from configuration to calls to controllers' functions.
"""
def __init__(self,
@@ -42,26 +50,6 @@ def __init__(self,
controller):
self.configuration = configuration
self.controller = controller
- self.editor = False
-
- def handle(self, input, *args, **kwargs):
- """Handle any input."""
-
- if self.is_keyboard_input(input):
- key = ''.join(input)
- self.handle_keyboard_input(key)
- else:
- # TODO mouse support
- return
-
- # -- Keyboard input --------------------------------------------------------
-
- def is_keyboard_input(self, input):
- """Return True if `input` is keyboard input."""
- if input:
- is_string = lambda s : isinstance(s, str) or isinstance(s, unicode)
- _and = lambda a, b: a and b
- return reduce(_and, map(is_string, input))
def is_bound(self, key, name):
"""
@@ -74,26 +62,26 @@ def is_bound(self, key, name):
else:
return key == bound_key
- def handle_keyboard_input(self, key):
- """Handle a keyboard input."""
- # Editor mode goes first
- if self.editor:
- size = 20,
- self.editor.keypress(size, key)
- return
-
+ def handle(self, key):
+ """Handle keyboard input."""
# Global commands
- self._turses_key_handler(key)
+ handled = not self._turses_key_handler(key)
+ if handled:
+ return
# Timeline commands
if not self.controller.is_in_help_mode():
- self._timeline_key_handler(key)
+ handled = not self._timeline_key_handler(key)
+ if handled:
+ return
if self.controller.is_in_info_mode():
return
# Motion commands
- self._motion_key_handler(key)
+ handled = not self._motion_key_handler(key)
+ if handled:
+ return
# Help mode commands
# only accepts motion commands, timeline commands and <Esc>
@@ -105,13 +93,21 @@ def handle_keyboard_input(self, key):
# Timeline mode commands
# Buffer commands
- self._buffer_key_handler(key)
+ handled = not self._buffer_key_handler(key)
+ if handled:
+ return
# Twitter commands
- self._twitter_key_handler(key)
+ handled = not self._twitter_key_handler(key)
+ if handled:
+ return
# External programs
- self._external_program_handler(key)
+ handled = not self._external_program_handler(key)
+ if handled:
+ return
+ else:
+ return key
def _turses_key_handler(self, key):
# quit
@@ -126,6 +122,8 @@ def _turses_key_handler(self, key):
# reload configuration
elif self.is_bound(key, 'reload_config'):
self.controller.reload_configuration()
+ else:
+ return key
def _motion_key_handler(self, key):
## up
@@ -140,6 +138,8 @@ def _motion_key_handler(self, key):
# scroll to bottom
elif self.is_bound(key, 'scroll_to_bottom'):
self.controller.scroll_bottom()
+ else:
+ return key
def _buffer_key_handler(self, key):
# Right
@@ -189,6 +189,8 @@ def _buffer_key_handler(self, key):
# Mark all as read
elif self.is_bound(key, 'mark_all_as_read'):
self.controller.mark_all_as_read()
+ else:
+ return key
def _timeline_key_handler(self, key):
# Show home Timeline
@@ -224,6 +226,8 @@ def _timeline_key_handler(self, key):
# Authors timeline
elif self.is_bound(key, 'user_timeline'):
self.controller.focused_status_author_timeline()
+ else:
+ return key
def _twitter_key_handler(self, key):
# Update timeline
@@ -265,17 +269,15 @@ def _twitter_key_handler(self, key):
# Tweet with hashtags
elif self.is_bound(key, 'tweet_hashtag'):
self.controller.tweet_with_hashtags()
+ else:
+ return key
def _external_program_handler(self, key):
# Open URL
if self.is_bound(key, 'openurl'):
self.controller.open_urls()
-
- # -- Editor ----------------------------------------------------------------
-
- def unset_editor(self):
- """Stop forwarding input to the editor."""
- self.editor = False
+ else:
+ return key
class Controller(object):
@@ -284,7 +286,6 @@ class Controller(object):
INFO_MODE = 0
TIMELINE_MODE = 1
HELP_MODE = 2
- EDITOR_MODE = 3
# -- Initialization -------------------------------------------------------
@@ -308,6 +309,8 @@ def __init__(self, configuration, ui, api_backend):
# start main loop
try:
self.main_loop()
+ except TweepError:
+ self.error_message(_('API error'))
except:
exit(1)
@@ -330,8 +333,9 @@ def init_timelines(self):
self.user = self.api.verify_credentials()
self.info_message(_('Initializing timelines'))
self.timelines = VisibleTimelineList()
- # TODO make default timeline list configurable
self.append_default_timelines()
+ seconds = self.configuration.update_frequency
+ self.loop.set_alarm_in(seconds, self.update_alarm)
def reload_configuration(self):
raise NotImplementedError
@@ -342,6 +346,11 @@ def api_init_error(self):
# TODO retry
self.error_message(_('Couldn\'t initialize API'))
+ def update_alarm(self, *args, **kwargs):
+ seconds = self.configuration.update_frequency
+ self.update_all_timelines()
+ self.loop.set_alarm_in(seconds, self.update_alarm)
+
# -- Modes ----------------------------------------------------------------
def timeline_mode(self):
@@ -383,15 +392,6 @@ def help_mode(self):
def is_in_help_mode(self):
return self.mode == self.HELP_MODE
- def editor_mode(self):
- """Activate editor mode."""
- self.mode = self.EDITOR_MODE
- self.key_handler.editor = self.ui.editor
-
- def is_in_editor_mode(self):
- return self.mode == self.EDITOR_MODE
-
-
# -- Timelines ------------------------------------------------------------
@wrap_exceptions
@@ -418,14 +418,36 @@ def append_timeline(self,
@async
def append_default_timelines(self):
- self.append_home_timeline()
- self.timeline_mode()
- for append in [self.append_mentions_timeline,
- self.append_favorites_timeline,
- self.append_direct_messages_timeline,
- self.append_own_tweets_timeline]:
- append()
- self.draw_timelines()
+ default_timelines = {
+ HOME_TIMELINE: self.append_home_timeline,
+ MENTIONS_TIMELINE: self.append_mentions_timeline,
+ FAVORITES_TIMELINE: self.append_favorites_timeline,
+ MESSAGES_TIMELINE: self.append_direct_messages_timeline,
+ OWN_TWEETS_TIMELINE: self.append_own_tweets_timeline,
+ }
+
+ timelines = [
+ HOME_TIMELINE,
+ MENTIONS_TIMELINE,
+ FAVORITES_TIMELINE,
+ MESSAGES_TIMELINE,
+ OWN_TWEETS_TIMELINE,
+ ]
+
+ is_any = any([self.configuration.default_timelines[timeline]
+ for timeline in timelines])
+
+ if is_any:
+ self.timeline_mode()
+ else:
+ self.info_message(_('You don\'t have any default timelines activated'))
+ return
+
+ for timeline in timelines:
+ append = default_timelines[timeline]
+ if self.configuration.default_timelines[timeline]:
+ append()
+ self.draw_timelines()
self.clear_status()
def append_home_timeline(self):
@@ -498,7 +520,10 @@ def append_direct_messages_timeline(self):
on_success=timeline_fetched,)
def append_thread_timeline(self):
- status = self.timelines.get_focused_status()
+ status = self.timelines.get_active_status()
+ if status is None:
+ return
+
timeline_fetched = partial(self.info_message,
_('Thread fetched'))
timeline_not_fetched = partial(self.error_message,
@@ -518,6 +543,7 @@ def append_thread_timeline(self):
on_error=timeline_not_fetched,
on_success=timeline_fetched)
+ @async
def update_all_timelines(self):
for timeline in self.timelines:
timeline.update()
@@ -588,7 +614,8 @@ def update_active_timeline_with_newer_statuses(self):
"""
active_timeline = self.timelines.get_active_timeline()
active_status = active_timeline.get_active()
- active_timeline.update_with_extra_kwargs(since_id=active_status.id)
+ if active_status:
+ active_timeline.update_with_extra_kwargs(since_id=active_status.id)
@async
def update_active_timeline_with_older_statuses(self):
@@ -597,7 +624,8 @@ def update_active_timeline_with_older_statuses(self):
"""
active_timeline = self.timelines.get_active_timeline()
active_status = active_timeline.get_active()
- active_timeline.update_with_extra_kwargs(max_id=active_status.id)
+ if active_status:
+ active_timeline.update_with_extra_kwargs(max_id=active_status.id)
def previous_timeline(self):
if self.timelines.has_timelines():
@@ -730,7 +758,6 @@ def redraw_screen(self):
def tweet_handler(self, text):
"""Handle the post as a tweet of the given `text`."""
- self.key_handler.unset_editor()
self.timeline_mode()
self.ui.remove_editor(self.tweet_handler)
self.ui.set_focus('body')
@@ -752,7 +779,6 @@ def tweet_handler(self, text):
def direct_message_handler(self, username, text):
"""Handle the post as a DM of the given `text` to `username`."""
- self.key_handler.unset_editor()
self.timeline_mode()
self.ui.remove_editor(self.direct_message_handler)
self.ui.set_focus('body')
@@ -779,11 +805,9 @@ def search_handler(self, text):
Handles creating a timeline tracking the search term given in
`text`.
"""
- if self.is_in_editor_mode():
- self.key_handler.unset_editor()
- self.timeline_mode()
- self.ui.remove_editor(self.search_handler)
- self.ui.set_focus('body')
+ self.timeline_mode()
+ self.ui.remove_editor(self.search_handler)
+ self.ui.set_focus('body')
if text is None:
self.info_message(_('Search cancelled'))
@@ -811,7 +835,6 @@ def search_user_handler(self, username):
"""
Handles creating a timeline tracking the searched user's tweets.
"""
- self.key_handler.unset_editor()
self.ui.remove_editor(self.search_user_handler)
self.ui.set_focus('body')
@@ -841,32 +864,38 @@ def search(self, text=None):
self.ui.show_text_editor(prompt='Search',
content=text,
done_signal_handler=self.search_handler)
- self.editor_mode()
def search_user(self):
self.ui.show_text_editor(prompt=_('Search user (no need to prepend it with "@")'),
content='',
done_signal_handler=self.search_user_handler)
- self.editor_mode()
def search_hashtags(self):
- status = self.timelines.get_focused_status()
+ status = self.timelines.get_active_status()
+ if status is None:
+ return
hashtags = ' '.join(get_hashtags(status))
self.search_handler(text=hashtags)
def focused_status_author_timeline(self):
- status = self.timelines.get_focused_status()
+ status = self.timelines.get_active_status()
+ if status is None:
+ return
author = get_authors_username(status)
self.append_user_timeline(author)
- def tweet(self):
- self.ui.show_tweet_editor(prompt=_('Tweet'),
- content='',
+ def tweet(self,
+ prompt=_('Tweet'),
+ content=''):
+ self.ui.show_tweet_editor(prompt=prompt,
+ content=content,
done_signal_handler=self.tweet_handler)
- self.editor_mode()
def retweet(self):
status = self.timelines.get_active_status()
+ if status is None:
+ return
+
if is_DM(status):
self.error_message(_('You can\'t retweet direct messages'))
return
@@ -881,6 +910,10 @@ def retweet(self):
def manual_retweet(self):
status = self.timelines.get_active_status()
+
+ if status is None:
+ return
+
rt_text = 'RT ' + status.text
if is_valid_status_text(' ' + rt_text):
self.tweet(content=rt_text)
@@ -888,8 +921,9 @@ def manual_retweet(self):
self.error_message(_('Tweet too long for manual retweet'))
def reply(self):
- status = self.timelines.get_focused_status()
-
+ status = self.timelines.get_active_status()
+ if status is None:
+ return
if is_DM(status):
self.direct_message()
return
@@ -904,32 +938,35 @@ def reply(self):
self.ui.show_tweet_editor(prompt=_('Reply to %s' % author),
content=' '.join(mentioned),
done_signal_handler=self.tweet_handler)
- self.editor_mode()
def direct_message(self):
status = self.timelines.get_active_status()
+ if status is None:
+ return
recipient = get_dm_recipients_username(self.user.screen_name, status)
if recipient:
self.ui.show_dm_editor(prompt=_('DM to %s' % recipient),
content='',
recipient=recipient,
done_signal_handler=self.direct_message_handler)
- self.editor_mode()
else:
self.error_message(_('What do you mean?'))
def tweet_with_hashtags(self):
- status = self.timelines.get_focused_status()
+ status = self.timelines.get_active_status()
+ if status is None:
+ return
hashtags = ' '.join(get_hashtags(status))
if hashtags:
# TODO cursor in the begginig
self.ui.show_tweet_editor(prompt=_('%s' % hashtags),
content=hashtags,
done_signal_handler=self.tweet_handler)
- self.editor_mode()
def delete_tweet(self):
status = self.timelines.get_active_status()
+ if status is None:
+ return
if is_DM(status):
self.delete_dm()
return
@@ -952,6 +989,8 @@ def delete_tweet(self):
def delete_dm(self):
dm = self.timelines.get_active_status()
+ if dm is None:
+ return
if dm.sender_screen_name != self.user.screen_name:
self.error_message(_('You can only delete messages sent by you'))
@@ -968,6 +1007,8 @@ def delete_dm(self):
def follow_selected(self):
status = self.timelines.get_active_status()
+ if status is None:
+ return
username = get_authors_username(status)
if username == self.user.screen_name:
self.error_message(_('You can\'t follow yourself'))
@@ -982,6 +1023,8 @@ def follow_selected(self):
def unfollow_selected(self):
status = self.timelines.get_active_status()
+ if status is None:
+ return
username = get_authors_username(status)
if username == self.user.screen_name:
self.error_message(_('That doesn\'t make any sense'))
@@ -996,6 +1039,8 @@ def unfollow_selected(self):
def favorite(self):
status = self.timelines.get_active_status()
+ if status is None:
+ return
favorite_error = partial(self.error_message,
_('Failed to mark tweet as favorite'))
favorite_done = partial(self.info_message,
@@ -1006,6 +1051,8 @@ def favorite(self):
def unfavorite(self):
status = self.timelines.get_active_status()
+ if status is None:
+ return
unfavorite_error = partial(self.error_message,
_('Failed to remove tweet from favorites'))
unfavorite_done = partial(self.info_message,
@@ -1021,6 +1068,8 @@ def open_urls(self):
Open the URLs contained on the focused tweets in a browser.
"""
status = self.timelines.get_active_status()
+ if status is None:
+ return
urls = get_urls(status.text)
if not urls:
@@ -1040,7 +1089,7 @@ def open_urls(self):
self.error_message(_('Unable to launch the browser'))
-class CursesController(Controller):
+class Turses(Controller):
"""Controller for the curses implementation."""
def main_loop(self):
@@ -1048,7 +1097,8 @@ def main_loop(self):
self.key_handler = KeyHandler(self.configuration, self)
self.loop = urwid.MainLoop(self.ui,
self.configuration.palette,
- input_filter=self.key_handler.handle,)
+ handle_mouse=False,
+ unhandled_input=self.key_handler.handle,)
self.loop.run()
def exit(self):
View
13 turses/models.py
@@ -469,8 +469,9 @@ def update_with_extra_kwargs(self, **extra_kwargs):
def get_unread_count(self):
def one_if_unread(tweet):
- readed = lambda tweet: getattr(tweet, 'read', False)
- return 0 if readed(tweet) else 1
+ if hasattr(tweet, 'read') and tweet.read:
+ return 0
+ return 1
return sum([one_if_unread(tweet) for tweet in self.statuses])
@@ -525,6 +526,10 @@ def mark_active_as_read(self):
if active_status:
active_status.read = True
+ def mark_all_as_read(self):
+ for status in self.statuses:
+ status.read = True
+
class TimelineList(UnsortedActiveList):
"""
@@ -646,10 +651,6 @@ def _mark_read(self):
active_timeline = self.get_active_timeline()
active_timeline.mark_active_as_read()
- def get_focused_status(self):
- active_timeline = self.get_active_timeline()
- return active_timeline.get_active()
-
def shift_active_previous(self):
active_index = self.active_index
previous_index = active_index - 1
View
28 turses/ui.py
@@ -32,7 +32,6 @@
connect_signal,
disconnect_signal
)
-from urwid import __version__ as urwid_version
from . import version
from .config import (
@@ -228,7 +227,12 @@ def show_dm_editor(self,
recipient=recipient,)
def remove_editor(self, done_signal_handler):
- disconnect_signal(self.editor, 'done', done_signal_handler)
+ try:
+ disconnect_signal(self.editor, 'done', done_signal_handler)
+ except:
+ # `disconnect_signal` raises an exception if no signal was
+ # connected from `self.editor`. We can safely ignore it.
+ pass
self.editor = None
self.clear_status()
@@ -737,8 +741,6 @@ class BoxDecoration(WidgetDecoration, WidgetWrap):
def __init__(self, original_widget, title=''):
self.color = 'header'
- if int(urwid_version[0]) == 1:
- utf8decode = lambda string: string
def use_attr(a, t):
if a:
@@ -748,32 +750,32 @@ def use_attr(a, t):
# top line
tline = None
tline_attr = Columns([('fixed', 2,
- Divider(utf8decode(""))),
+ Divider(u"")),
('fixed', len(title),
AttrMap(Text(title), self.color)),
- Divider(utf8decode("")),])
+ Divider(u""),])
tline = use_attr(tline, tline_attr)
# bottom line
bline = None
- bline = use_attr(bline, Divider(utf8decode("")))
+ bline = use_attr(bline, Divider(u""))
# left line
lline = None
- lline = use_attr(lline, SolidFill(utf8decode("")))
+ lline = use_attr(lline, SolidFill(u""))
# right line
rline = None
- rline = use_attr(rline, SolidFill(utf8decode("")))
+ rline = use_attr(rline, SolidFill(u""))
# top left corner
tlcorner = None
- tlcorner = use_attr(tlcorner, Text(utf8decode("")))
+ tlcorner = use_attr(tlcorner, Text(u""))
# top right corner
trcorner = None
- trcorner = use_attr(trcorner, Text(utf8decode("")))
+ trcorner = use_attr(trcorner, Text(u""))
# bottom left corner
blcorner = None
- blcorner = use_attr(blcorner, Text(utf8decode("")))
+ blcorner = use_attr(blcorner, Text(u""))
# bottom right corner
brcorner = None
- brcorner = use_attr(brcorner, Text(utf8decode("")))
+ brcorner = use_attr(brcorner, Text(u""))
# top
top = Columns([('fixed', 1, tlcorner),
Please sign in to comment.
Something went wrong with that request. Please try again.