identicurse-0.9+dfsg0/0000755000175000017500000000000011724255646013707 5ustar mvdanmvdanidenticurse-0.9+dfsg0/misc/0000755000175000017500000000000011720536557014641 5ustar mvdanmvdanidenticurse-0.9+dfsg0/misc/colours/0000755000175000017500000000000011720536557016327 5ustar mvdanmvdanidenticurse-0.9+dfsg0/misc/colours/nautiloid.json0000644000175000017500000000277511720536557021225 0ustar mvdanmvdan{ "colours": { "username": [ "cyan", "none" ], "pause_line": [ "white", "red" ], "notice": [ "none", "none" ], "group": [ "black", "brown" ], "profile_fields": [ "blue", "none" ], "statusbar": [ "black", "white" ], "warning": [ "black", "red" ], "none": [ "none", "none" ], "selector": [ "brown", "none" ], "tabbar_active": [ "red", "white" ], "source": [ "green", "none" ], "profile_values": [ "none", "none" ], "tag": [ "brown", "none" ], "notice_link": [ "green", "none" ], "profile_title": [ "cyan", "none" ], "tabbar": [ "white", "red" ], "time": [ "brown", "none" ], "timelines": [ "none", "none" ], "notice_count": [ "blue", "none" ], "search_highlight": [ "white", "blue" ] } } identicurse-0.9+dfsg0/.gitignore0000644000175000017500000000001211720536557015667 0ustar mvdanmvdan*.py[o|c] identicurse-0.9+dfsg0/CHANGELOG0000644000175000017500000003477311720536557015136 0ustar mvdanmvdan0.9: - "from ostatus" is now replaced by "from $INSTANCE_URL" for remote notices. - Thanks to @moggers87, we now have mostly functional UTF text entry support (i.e., non-ASCII characters work properly) - true support would depend on Python 2.x actually getting a curses module with proper wide-char support. - Slogans now appear instead of the "doing nothing" message. Can be restored to old behaviour by setting "status_slogans" to false in config. - Tabs are now numbered - for tabs 1-9, the number is the same one you press to switch straight to it. Can be disabled by setting "enumerate_tabs" to false in config. - Border is now off by default. However, if you already set "border" in config, it will remain how you set it. - New slogans, as usual. - Bugfixes for: - Non-sane application of "new_reply_mode" when splitting notices. - All the "NoneType has no length" errors so far unearthed. - Crashes on "/user (n)"-type commands, where (n) is a notice number. 0.8.2: - Fixed broken setup_py2exe.py. - Fixed broken OAuth code. 0.8.1: - Fixed broken setup.py. 0.8: - New slogans. - Pausing (p to toggle pause on the current tab, P for all tabs), with a pause line to indicate which notices are before/after a pause. - Unfavouriting notices (command /unfavourite, or hotkey F). - Z (shift+z) keybinding to move to last dent in timeline. - Config is now stored in ~/.identicurse/config.json. Any existing setups will be automatically converted upon start. - Colourschemes can now be stored as discrete files, and pulled in with the "colorscheme" config key. Sample colourscheme provided under misc/. - Default keybindings can now be fully overriden. - d/D default bindings are now reply mode/reply respectively, the opposite way round to in 0.7, since reply mode is generally more useful. - L (shift+l) keybinding to display notice links, for use in (for example) linking others to a specific dent. - Support for Status.net 1.0's conversation API, allowing full conversations to be viewed at last. (only on 1.0+ instances) - Support for displaying "user -> user"-style addressee information on 1.0+ instances. - Moved to using Leah Culver's python-oauth module (MIT licensed), resolves some weird OAuth edgecase bugs. - /+ and /- bindings for cycling through tabs (second pair cycles backwards). - New regex-based filtering mode. More complex, but more flexible. - m and M keybindings, and /mute and /unmute commands, for muting/unmuting entire conversations. Any notice in a muted conversation will simply not be displayed. (only on 1.0+ instances) - Message-splitting behaviour adjusted to make more sense with 1.0 instances. - "new_reply_mode" config key for not auto-prepending @usernames when starting reply mode. - Bugfixes for: - IdentiCurse gave a "bad credentials" error on trying to use valid credentials, if the Status.net instance was down. - Some "currently-selected dent" commands crashed the whole program on exceptions. - /link would not match ftp:// URLs. - /link commands would crash IdentiCurse if no link was present in the target dent. - Repeating wouldn't work on 1.0+ instances. 0.7.3: - Tab completion is now more reliable across different systems. - When deleting characters from multiline input boxes, the remaining lines are correctly moved over the line boundary. - "/link [notice number]" no longer causes a crash - Odd edge case fixed, where username/tag/group identification within a dent misidentified an entire dent as a single username/tag/group. - An import bug that prevented running successfully on python 2.5 is now fixed. 0.7.2: - The favourites tab no longer crashes on load. 0.7.1: - No longer gets the wrong path on initial setup on Windows, which would crash it. 0.7: - New slogans. - The screen will now flash when an update fetches new mentions/direct messages. - Smoother scrolling when using a/s to switch notices; when moving to a notice that is partially/fully off the screen, IdentiCurse will now only scroll enough to make it visible, rather than scroll by an entire page. Controlled by the 'smooth_cscroll' config option, default true. - /quote command added, for adding/changing text when repeating a notice. - If HTTPS is not used when initially creating an account, IdentiCurse now double-checks that this is definitely what the user wants. - There is now a tab bar, which displays all tabs in order. This replaces the former "Tab x: Name" that was on the right of the status bar. - UI elements can now be placed in an arbitrary order, with the default layout remaining the same as the fixed layout of previous versions (except for the addition of the tab bar). See README for syntax details. - OAuth is now supported, and can be enabled either on initial setup, or by setting the config option 'use_oauth' to true. - Links for the web view of each notice can be displayed after them. Config option 'show_notice_links', default false. - Size of the input box can now be changed, by setting the 'length_override' config option to the minimum number of characters that should be able to fit in the input box. - Tab completion of usernames, groups, and tags. Determines whether to use groups/tags by looking for # or ! at the start of the word. Defaults to usernames if either @ or no sign present. Only aware of users/groups/tags already seen, unless config option 'prefill_user_cache' is set to true (this will result in all users you follow being available for tab completion, at the cost of slower startup). - Tab completion of commands, identified by an initial /. - URLs will be shortened using ur1.ca on pressing tab immediately after them (as if you were trying to tab-complete them). - In-development (-dev) versions now have distinct codenames from their corresponding final releases, though they share the same initial letter. - /quit command, has the same effect as the q keybinding. - 'initial_tabs' now supports the favourites tab type, and also has a terser syntax for some tab types. See README for more detail. - "Updating" status message now also states which tab is currently being updated. - Reply mode: normal replying, but with the ability to edit the entire notice, rather than automatically getting the target user's name added. - Compact mode is now extremely compact, but disabled by default. The new non-compact is the same as 0.6's compact. - 'show_source' option, to show/hide the source messages ("from $client") for notices. Default is true. - Refreshing now only fetches notices not already received, resulting in speed improvements for refreshing. - The cursor is now hidden when not in the input box. - There is now support for building on Windows. - The terminal's title is now set to "IdentiCurse". - Messages from the --colour-support command-line option are now more correct. - New keybindings (case sensitive: E is different to e, for example): - : goes into insert mode with an initial / already present. Useful for quick commands. - E goes into quote mode with the currently selected notice. - D goes into reply mode with the currently selected notice. - , moves the current tab one place left in the tab bar. - . moves the current tab one place right in the tab bar. - # deletes the currently selected notice. - Bugfixes for: - HOME/END do not work in input box. - HTML entities are not expanded when encountered during remote notice expansion. - All known remaining reflowing bugs. - IdentiCurse crashes on very fast resizing of window. - IdentiCurse crashes when certain Unicode characters are present in a notice and are not supported by the user's system. - Notice time/date is displayed incorrectly when the instance has non-zero offset in its timestrings. - IdentiCurse crashes on attempting to view the profile of a non-existent user. - Spaces are discarded when at the start/end of lines in multi-line input box. 0.6.4: - API implementation changed to fit with changes to the Status.net API. 0.6.3: - Buffer errors on Cygwin fixed. 0.6.2: - Fixed broken codepath encountered only by upgrading users. 0.6.1: - Colours enabled by default. 0.6: - New slogans. - Borders can now be switched off, with the 'border' config option. - Notices can now be displayed in compact mode (new default), or non-compact mode (as in old versions), as dictated by the 'compact_notices' config option.. - Catch typos like "/r1 foo", and automatically treat as "/r 1 foo" (favours exactly what was typed if it's interpretable as valid). - Added the ability to colour various parts of the application either manually or with a default colour scheme. You can turn it off. - Automatic configuration is now done on first run if you don't have a config - no need to manually set up config any more. - Reflowing now tries to split on word boundaries where possible - less words split over lines. - Notice times given are now approximate. - Config is now looked for at ~/.identicurse, then /etc/identicurse.conf. The latter is recommended only if you absolutely need more than one user to share a single config file. - New /alias command for creating aliases from within IdentiCurse. - Usernames in notices are highlighted in the same colour as usernames in the notice details. - User, tag, and group rainbows, where each user/group/tag is assigned a random colour that persists wherever that user/group/tag is mentioned in the timeline. Config options are "user_rainbow", "group_rainbow", "tag_rainbow", possible values true or false. - Experimental expansion for remote notices that were truncated due to local instance having shorter length_limit. Config option is "expand_remote", possible values true or false. - Search results are now highlighted (provided colour is enabled). - Bugfixes for: - Time is several hours out for notices whose post time (according to the server) is ahead of local time. - Profile crashes if URL, Bio or Location is not set for a user. 0.5.1: - Initial setup documentation clarified. - Failure to revert to correct status after in-page search fixed. - README reflowed to thinner width. 0.5: - New slogans. - Directs can now be sent by notice number as well as by name. - In-page search, activated with / key. n/N can be used to quickly move between matches. - Dents that begin with / but aren't commands will now be sent as normal dents. - /reply in DM tabs is treated as /direct, to prevent messages intended to be private from accidentally being sent publicly. - Tab-switching is now keys < and >. - Quick Reply: Can now type 'l' then a number between 1-9 to quickly reply to that dent. - Current Reply, Current Favourite, Current Repeat, and Current Context: Can now type 'd' to reply to the currently selected dent, 'f' to favourite it, 'e' to repeat it, or 'c' to view its context. Use 'a' and 's' to switch current dent, or 'z' to switch back to the first dent in the current page. - Can now press = to go back to the top of the newest page (in paged tabs). - Page number indicator in tab names. - Character counter for insert mode. - Slogans will now be read from ~/.identicurse_slogans (see README for format details) if it exists. - Repeats are now displayed and handled the same as their source dent (except that the user section of the dent gains a "repeat by" section on repeats). - Bugfixes for: - Split statuses cause crashes if set to preserve the first word (i.e., replies, bug reports, and feature requests). - Timestamp is " ago" if the dent was created within a second of current time. (Fixed timestamp in this situation is "Now".) 0.4.1: - Incorrect license in setup.py fixed. 0.4: - Tabs know if they are active, so the display can be updated for the active tab as soon as its data is finished updating. - Links in a notice can now be opened in a browser with the /link command. - Daily average for number of notices. - New slogans. - SPACE/b for moving down/up a whole screen's worth. - HOME/g for scrolling to the top of the page, END/G for scrolling to the bottom of the page. - Moving to older/newer pages automatically scrolls to the top/bottom, respectively. - Keybindings can be customised in config. - HTTP errors when communicating with the SN instance now display in the status-bar instead of messing up the screen. - New split indicator .., shorter than the original (...). - /groupmember command for checking group membership. - Bugfixes for: - Viewing profiles where bio/location contains non-ascii characters causes an exception. 0.3.2: - Incorrect version number in setup.py fixed. 0.3.1: - Incorrect first-run instructions fixed. 0.3: - Filters - Adjust number of notices fetched per page (all timelines excluding Public, Search, Favourites) - Scrolling with j/k. - Leaving insert mode with ESC. - DEL now functions correctly in insert mode. - Tab switching with n/p. - New slogans. - Clean reflowing of buffers to prevent overflowing the screen and causing crashes. - Aliases can alias to commands with parameters included, instead of just bare commands. - Commands can now contain an arbitrary number of spaces between parameters. - Long dent handling now defaults to splitting. - Context view now follows repeats back to the original notice, as if they were a reply - repeats that can be followed this way are denoted by [~]. - Expanded profile pages. - As of this version, IdentiCurse is distributed as an installable package, using setuptools. - Notices posted are now automatically shown on the timeline without refreshing every time (faster) - More detail in StatusNet error messages. - Bugfixes for: - Exceptions occurred when ending, even on clean quit. 0.2.2: - Textbox crashing fixed. 0.2.1: - Enter now works to submit in multiline text-boxes. - Python 2.5 compatibility. 0.2: - Long dent handling, default behaviour is to truncate. - IdentiCurse now looks for config in ~/.identicurse before trying ./config.json. - IdentiCurse inherits terminal colours (previously defaulted to an ugly grey on some terms). - Multiline input, requires CTRL+G to submit. - Stacked tab order, so you return to the last tab you were on when closing another. - Timelines auto-update, except while in insert mode. - Dents have timestamps. - Paged timelines. - Automatic resizing of windows now results in IdentiCurse also resizing. - Initial tabset is now configurable. - /config command. - /home, /mentions, /direct, and /public commands. - Correctly handles unlimited-length StatusNet instances. - Bugfixes for: - User timelines could not be viewed with /user [notice number] syntax. - Profile pages get duplicate information on refresh. identicurse-0.9+dfsg0/res/0000755000175000017500000000000011720536557014477 5ustar mvdanmvdanidenticurse-0.9+dfsg0/res/identicurse.ico0000644000175000017500000003201511720536557017512 0ustar mvdanmvdanF00U  #h0PNG  IHDR\rfIDATxԱ^WEwıq8v"Wb(z`̼sJwp{{^/Jo/k \Lb.>m'F~'ᅫ~_ @]&Pp6r Ml .g(w8@ @]&Pp6r Ml .g(w8@ @]&Pp6r Ml .g[OAwRO߿4`7 @Hv`7 @Hv`7 @Hv`7 @Hv`7 @Hv`7 @Hv`7 @Hv`7 @Hv`7 @Hv`7 @Hv`7 @Hv`7 @Hv& M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 # M2 "0K2Y`d $ #!% ,@Hf @FB0K2Y`d $ #!% ,@Hf @FB0K2Y`d $ #!% ,@Hf @FB0K2Y`d $ #!% ,@Hf @FB0w ٦& M( M( M( M( M( M( M( M( M( M( M( 8.&p1 \Lb.&p1 \Lb.&p1 \LbkקOo/?~.&p1 \Lb.&p1 \Lb.&p1 \LbKb.&p1!jhTIENDB`(0` Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ԝ ԝ ԝ ԝ ԝ ԝ ԝ ԝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ՞ ՞ ՞ ՞ ՞ ՞ ՞ ԝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ՞ ՞ ՞ ՞ ՞ ՞ ՞ ԝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ՞ ՞ ՞ ՞ ՞ ՞ ՞ ԝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ՞ ՞ ՞ ՞ ՞ ՞ ՞ ԝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ՞ ՞ ՞ ՞ ՞ ՞ ՞ ԝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ՞ ՞ ՞ ՞ ՞ ՞ ՞ ԝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ՞ ՞ ՞ ՞ ՞ ՞ ՞ ԝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ՞ ՞ ՞ ՞ ՞ ՞ ՞ ԝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ՞ ՞ ՞ ՞ ՞ ՞ ՞ ԝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ՞ ՞ ՞ ՞ ՞ ՞ ՞ ԝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ԝ ԝ ԝ ԝ ԝ ԝ ՞ ԝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ԝ ԝ ԝ ԝ ԝ ԝ ՞ ԝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ԝ ԝ ԝ ԝ ԝ ԝ ՞ ԝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ԝ ԝ ԝ ԝ ԝ ԝ ՞ ԝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ԝ ԝ ԝ ԝ ԝ ԝ ՞ ԝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ԝ ԝ ԝ ԝ ԝ ԝ ՞ ԝ Ӝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ Ӝ Ӝ Ӝ Ӝ ԝ ԝ ԝ ԝ ԝ ԝ ՞ ՞ ՞ ՞ ՞ ՞ ԝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ӝ ԝ ԝ Ӝ ԝ ԝ ԝ ԝ ԝ ԝ ՞ ՞ ՞ ՞ ՞ ՞ ԝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ Ӝ ԝ ԝ Ӝ ԝ ԝ ԝ ԝ ԝ ԝ ՞ ՞ ՞ ՞ ՞ ՞ ԝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ Ӝ Ӝ ӝ Ӝ ԝ ԝ ԝ ԝ ԝ ԝ ՞ ՞ ՞ ՞ ՞ ՞ ԝΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ΐ ( @ ԝ ԝ ԝ ԝ ԝ ԝ ԝ ԝ ԝ ԝ ԝ ԝϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ԝ ԝ ԝ ԝ ԝ ԝ ԝ ԝ ԝ ԝ ԝ ԝϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ԝ ԝ ԝ ԝ ԝ ԝ ԝ ԝ ԝ ԝ ԝ ԝϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ԝ ԝ ԝ ԝ ԝ ԝϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ԝ ԝ ԝ ԝ ԝ ԝϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ԝ ԝ ԝ ԝ ԝ ԝϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ԝ ԝ ԝ ԝ ԝ ԝϑ ϑ ϑ ϑ ϑ ϑ ԝ ԝ ԝ ԝ ԝ ԝϑ ϑ ϑ ϑ ϑ ϑ ԝ ԝ ԝ ԝ ԝ ԝϑ ϑ ϑ ϑ ϑ ϑ ԝ ԝ ԝ ԝ ԝ ԝϑ ϑ ϑ ϑ ϑ ϑ ՞ ՞ ՞ ՞ ՞ ԝϑ ϑ ϑ ϑ ϑ ϑ ՞ ՞ ՞ ՞ ՞ ԝϑ ϑ ϑ ϑ ϑ ϑ ՞ ՞ ՞ ՞ ՞ ԝϑ ϑ ϑ ϑ ϑ ϑ ՞ ՞ ՞ ՞ ՞ ԝϑ ϑ ϑ ϑ ϑ ϑ ՞ ՞ ՞ ՞ ՞ ԝϑ ϑ ϑ ϑ ϑ ϑ ՞ ՞ ՞ ՞ ՞ ԝϑ ϑ ϑ ϑ ϑ ϑ ՞ ՞ ՞ ՞ ՞ ԝϑ ϑ ϑ ϑ ϑ ϑ ՞ ՞ ՞ ՞ ՞ ԝϑ ϑ ϑ ϑ ϑ ϑ ՞ ՞ ՞ ՞ ՞ ԝϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ՞ ՞ ՞ ՞ ՞ ԝϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ՞ ՞ ՞ ՞ ՞ ԝϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ԝ ԝ ԝ ՞ ՞ ՞ ՞ ՞ ՞ ՞ ՞ ԝϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ԝ ԝ ԝ ՞ ՞ ՞ ՞ ՞ ՞ ՞ ՞ ԝϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ԝ ԝ ԝ ՞ ՞ ՞ ՞ ՞ ՞ ՞ ՞ ԝϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ ϑ (  ԝ ԝ ԝ ԝ ԝϑ ϑ ϑ ϑ ϑ ԝ ԝ ԝϑ ϑ ϑ ϑ ԝ ԝ ԝϑ ϑ ϑ ԝ ԝ ԝϑ ϑ ϑ ԝ ԝ ԝϑ ϑ ϑ ՞ ՞ ԝϑ ϑ ϑ ՞ ՞ ԝϑ ϑ ϑ ՞ ՞ ԝϑ ϑ ϑ ՞ ՞ ԝϑ ϑ ϑ ՞ ՞ ԝϑ ϑ ϑ ϑ ԝ ՞ ՞ ՞ ՞ϑ ϑ ϑ ϑ ϑ AAAAAAAAAAAAAAAAidenticurse-0.9+dfsg0/setup_py2exe.py0000644000175000017500000000541111720536557016715 0ustar mvdanmvdan#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # Copyright (C) 2010-2012 Reality and Psychedelic Squid # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ py2exe build script. Requires that you have an appropriate curses library, such as http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses installed, and will do until such time as Python finally includes Windows curses support as standard. """ __docformat__ = 'restructuredtext' import distutils from setuptools import setup, find_packages import py2exe try: distutils.dir_util.remove_tree("build", "dist", "src/identicurse.egg-info") except: pass setup( name="identicurse", version='0.9', description="A simple Identi.ca client with a curses-based UI.", long_description=("A simple Identi.ca client with a curses-based UI."), author="Psychedelic Squid and Reality", author_email='psquid@psquid.net and tinmachin3@gmail.com', url="http://identicurse.net/", download_url=("http://identicurse.net/release/"), license="GPLv3+", data_files=[('',['README', 'conf/config.json'])], packages=find_packages('src'), package_dir={'': 'src'}, include_package_data=True, entry_points={ 'console_scripts': ['identicurse = identicurse:main'], }, console=[{ "script": 'src/identicurse/__init__.py', "icon_resources": [(1, 'res/identicurse.ico')], "dest_base": 'identicurse', }], zipfile=None, options={ "py2exe": { "compressed": 1, "optimize": 2, "ascii": 1, "bundle_files": 1, "packages": 'encodings, identicurse', "includes": 'identicurse.config, identicurse.textbox, identicurse.helpers, identicurse.statusbar, identicurse.statusnet, identicurse.tabbage, identicurse.tabbar', "dll_excludes": 'w9xpopen.exe', # unneeded, since we don't intend to be runnable on Win9x } }, classifiers=[ 'License :: OSI Approved :: GNU General Public License (GPL)', 'Development Status :: 5 - Production/Stable', 'Programming Language :: Python', ], ) identicurse-0.9+dfsg0/INSTALL0000644000175000017500000000230711720536557014741 0ustar mvdanmvdanINSTALLATION If you already have setuptools installed, installation is a simple two-step process: 1. As root, run setup.py: # python setup.py install 2. Copy the example config file into your home directory as .identicurse: $ cp conf/config.json ~/.identicurse And you're done! Read the README to get up to speed on configuring IdentiCurse to your liking, or (this is recommended for most users) just run "identicurse" straight away to get going with an automatically- generated config. If you do not have setuptools installed, you will need to either install your distribution's package for it, or manually install it if your distribution does not provide a package (instructions for which will not be given here). The package name for setuptools in various distributions is known to be: python-setuptools in Fedora, Ubuntu, Debian, openSUSE python2-distribute in Arch pysetuptools in Slackware devel/py-setuptools in OpenBSD, FreeBSD (if your distribution of choice is not in this list yet, feel free to inform us of the correct package name) Once you have setuptools installed, you can proceed with the install as above. identicurse-0.9+dfsg0/src/0000755000175000017500000000000011721406611014461 5ustar mvdanmvdanidenticurse-0.9+dfsg0/src/identicurse/0000755000175000017500000000000011720536557017013 5ustar mvdanmvdanidenticurse-0.9+dfsg0/src/identicurse/statusbar.py0000644000175000017500000000525511720536557021404 0ustar mvdanmvdan# -*- coding: utf-8 -*- # # Copyright (C) 2010-2012 Reality and Psychedelic Squid # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import threading, random, time, config, curses class StatusBar(object): def __init__(self, window): self.window = window self.text = "" self.timed_update_restore_value = None def timed_update(self, text, delay=10): TimedUpdate(self, text, delay).start() def update(self, text): if self.timed_update_restore_value is None: self.text = text self.redraw() else: self.timed_update_restore_value = text def redraw(self): self.window.erase() maxx = self.window.getmaxyx()[1] - 2 if len(self.text) >= (maxx): # if the left text would end up too near the right text self.window.addstr(0, 1, self.text[:maxx-3].strip() + "...") else: self.window.addstr(0, 1, self.text) self.window.refresh() def do_nothing(self): if config.config['status_slogans']: self.update("IdentiCurse: %s" % random.choice(config.session_store.slogans)) else: self.update("Doing nothing.") class TimedUpdate(threading.Thread): def __init__(self, statusbar, text, delay): threading.Thread.__init__(self) self.statusbar = statusbar self.text = text self.delay = delay def run(self): temp_restore_value = self.statusbar.text # store so we can set without trigerring the else clause self.statusbar.update(self.text) self.statusbar.timed_update_restore_value = temp_restore_value time.sleep(self.delay) if self.statusbar.timed_update_restore_value is not None: # make sure it wasn't already reset by another timed update, because we'd end up setting it to None, then, which just fucks everything up temp_restore_value = self.statusbar.timed_update_restore_value # as above self.statusbar.timed_update_restore_value = None self.statusbar.update(temp_restore_value) identicurse-0.9+dfsg0/src/identicurse/tabbage.py0000644000175000017500000010063611720536557020760 0ustar mvdanmvdan# -*- coding: utf-8 -*- # # Copyright (C) 2010-2012 Reality and Psychedelic Squid # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from helpers import DATETIME_FORMAT import os.path, re, sys, threading, datetime, locale, curses, random, httplib import identicurse, config, helpers from operator import itemgetter from statusnet import StatusNetError class Buffer(list): def __init__(self): list.__init__(self) def append(self, item): clean_item = [] for block in item: clean_blocks = [] try: if "\n" in block[0]: clean_blocks.append((block[0].split("\n")[0].replace("\r", ""), block[1])) for sub_block in block[0].split("\n")[1:]: clean_blocks.append((" "+sub_block.replace("\r", ""), block[1])) else: clean_blocks.append(block) for block in clean_blocks: if "\t" in block[0]: block = (" ".join(block[0].split("\t")), block[1]) clean_text = "" for char in block[0]: try: clean_text += char.encode(sys.getfilesystemencoding()) except UnicodeEncodeError: clean_text += "?" clean_block = (clean_text, block[1]) clean_item.append(clean_block) except TypeError: raise Exception(item) list.append(self, clean_item) def clear(self): self[:] = [] def reflowed(self, width): """ Return a reflowed-for-width copy of the buffer as a list. """ reflowed_buffer = [] for raw_line in self: reflowed_buffer.append([]) line_length = 0 line = raw_line[:] line.reverse() # reverse the line so we can use it as a stack while len(line) > 0: block = line.pop() try: # attempt to consider possibly different unicode length vs. ascii length block_len = len(block[0].encode(sys.getfilesystemencoding())) except: block_len = len(block) if (block_len + line_length) > width: split_point = helpers.find_split_point(block[0], width - line_length) reflowed_buffer[-1].append((block[0][:split_point], block[1])) # add the first half of the block as usual reflowed_buffer.append([]) line.append((block[0][split_point:], block[1])) # put the rest of the block back on the stack line_length = 0 else: reflowed_buffer[-1].append(block) line_length += block_len return reflowed_buffer class TabUpdater(threading.Thread): def __init__(self, tabs, callback_object, callback_function): threading.Thread.__init__(self) self.daemon = True self.tabs = tabs self.callback_object = callback_object self.callback_function = callback_function def run (self): config.session_store.update_error=None for tab in self.tabs: try: self.callback_object.status_bar.update("Updating '%s'..." % (tab.name)) tab.update() except StatusNetError, e: config.session_store.update_error="Status.Net error %d in '%s': %s" % (e.errcode, tab.name, e.details) if tab.active: tab.display() # update the display of the tab if it's the foreground one fun = getattr(self.callback_object, self.callback_function) fun() class Tab(object): def __init__(self, window): self.window = window self.buffer = Buffer() self.start_line = 0 self.html_regex = re.compile("<(.|\n)*?>") self.page = 1 self.search_highlight_line = -1 self.active = False self.paused = False def prevpage(self, n=1): if hasattr(self, "timeline"): if n > 0: self.page -= n if self.page < 1: self.page = 1 return False self.scrolldown(0) # scroll to the end of the newer page, so the dent immediately after the start of the last page can be seen self.chosen_one = len(self.timeline) - 1 else: if self.page == 1: return False else: self.page = 1 self.scrollup(0) self.chosen_one = 0 return True else: return False def nextpage(self): if hasattr(self, "timeline"): self.page += 1 self.scrollup(0) # as in prevpage, only the other way around self.chosen_one = 0 return True else: return False def scrollup(self, n): # n == 0 indicates maximum scroll-up, i.e., scroll right to the top if not (n == 0): self.start_line -= n if self.start_line < 0: self.start_line = 0 else: self.start_line = 0 def scrolldown(self, n): # n == 0 indicates maximum scroll-down, i.e., scroll right to the bottom maxy, maxx = self.window.getmaxyx()[0], self.window.getmaxyx()[1] if not (n == 0): self.start_line += n if self.start_line > len(self.buffer.reflowed(maxx - 2)) - (maxy - 3): self.start_line = len(self.buffer.reflowed(maxx - 2)) - (maxy - 3) else: self.start_line = len(self.buffer.reflowed(maxx - 2)) - (maxy - 3) def scrollto(self, n, force_top=True): # attempt to get line number n onto the screen (to the top if force_top==True) - this is less clean than the relative scrolls, so don't call it unless you *need* to go to a specific line. maxy, maxx = self.window.getmaxyx()[0], self.window.getmaxyx()[1] if (n >= self.start_line) and (n < (maxy - 3 + self.start_line)) and not force_top: # if the line is already visible and force_top==False, bail out return if (n > self.start_line) and (n > len(self.buffer.reflowed(maxx - 2)) - (maxy - 3)) and force_top: self.start_line = len(self.buffer.reflowed(maxx - 2)) - (maxy - 3) elif n < self.start_line or force_top: self.start_line = n else: self.start_line = n - (maxy - 4) def scrolltodent(self, n, smooth_scroll=False): maxy, maxx = self.window.getmaxyx()[0], self.window.getmaxyx()[1] if n < 9: nout = " " + str(n+1) else: nout = str(n+1) dent_line = 0 found_dent = False for line in self.buffer.reflowed(maxx - 2): for block in line: if block[1] == identicurse.colour_fields['notice_count'] and block[0] == nout: if smooth_scroll and (dent_line >= (maxy - 3 + self.start_line)): buffer_cache = self.buffer.reflowed(maxx - 2) while True: if dent_line == len(buffer_cache) - 1: break line_length = 0 for block in buffer_cache[dent_line+1]: line_length += len(block[0]) if line_length == 0: break dent_line += 1 self.scrollto(dent_line, force_top=False) elif (dent_line >= (maxy - 3 + self.start_line)) or (dent_line < self.start_line): self.scrollto(dent_line, force_top=True) return dent_line += 1 def display(self): maxy, maxx = self.window.getmaxyx()[0], self.window.getmaxyx()[1] self.window.erase() buffer = self.buffer.reflowed(maxx - 2) line_num = self.start_line for line in buffer[self.start_line:maxy - 3 + self.start_line]: remaining_line_length = maxx - 2 try: for (part, attr) in line: if attr == identicurse.colour_fields["pause_line"]: # we want pause lines to fill the width self.window.addstr("-"*(remaining_line_length-1), curses.color_pair(identicurse.colour_fields['pause_line'])) if line_num == self.search_highlight_line: remaining_line_length -= len(part) self.window.addstr(part, curses.color_pair(identicurse.colour_fields['search_highlight'])) else: self.window.addstr(part, curses.color_pair(attr)) if line_num == self.search_highlight_line: self.window.addstr(" "*remaining_line_length, curses.color_pair(identicurse.colour_fields['search_highlight'])) if line_num <= (maxy - 3 + self.start_line): self.window.addstr("\n") except: # if we somehow already hit the bottom (maybe there were weird chars?) pass # just ignore it and move on line_num += 1 self.window.refresh() class Help(Tab): def __init__(self, window, identicurse_path): self.name = "Help" self.path = os.path.join(identicurse_path, "README") Tab.__init__(self, window) def update(self): self.update_buffer() def update_buffer(self): self.buffer.clear() for l in open(self.path, 'r').readlines(): self.buffer.append([(l, identicurse.colour_fields['none'])]) class Timeline(Tab): def __init__(self, conn, window, timeline, type_params={}): self.conn = conn self.timeline = [] self.prev_page = -1 if not hasattr(config.session_store, 'user_cache'): config.session_store.user_cache = {} if not hasattr(config.session_store, 'tag_cache'): config.session_store.tag_cache = {} if not hasattr(config.session_store, 'group_cache'): config.session_store.group_cache = {} self.timeline_type = timeline self.type_params = type_params self.chosen_one = 0 if self.timeline_type == "user": self.basename = "@%s" % self.type_params['screen_name'] elif self.timeline_type == "tag": self.basename = "#%s" % self.type_params['tag'] elif self.timeline_type == "group": self.basename = "!%s" % self.type_params['nickname'] elif self.timeline_type == "search": self.basename = "Search: '%s'" % self.type_params['query'] elif self.timeline_type == "sentdirect": self.basename = "Sent Directs" else: self.basename = self.timeline_type.capitalize() self.name = self.basename Tab.__init__(self, window) def update_name(self): if self.page > 1: self.name = self.basename + "+%d" % (self.page - 1) else: self.name = self.basename if self.paused: self.name = self.name + " (paused)" def update(self): self.update_name() if self.paused: self.update_buffer() return get_count = config.config['notice_limit'] if self.prev_page != self.page: self.timeline = [] last_id = 0 if len(self.timeline) > 0: for notice in self.timeline: if notice["ic__from_web"]: # don't consider inserted posts latest last_id = notice['id'] break if self.timeline_type == "home": raw_timeline = self.conn.statuses_home_timeline(count=get_count, page=self.page, since_id=last_id) elif self.timeline_type == "mentions": raw_timeline = self.conn.statuses_mentions(count=get_count, page=self.page, since_id=last_id) elif self.timeline_type == "direct": raw_timeline = self.conn.direct_messages(count=get_count, page=self.page, since_id=last_id) elif self.timeline_type == "user": raw_timeline = self.conn.statuses_user_timeline(user_id=self.type_params['user_id'], screen_name=self.type_params['screen_name'], count=get_count, page=self.page, since_id=last_id) elif self.timeline_type == "group": raw_timeline = self.conn.statusnet_groups_timeline(group_id=self.type_params['group_id'], nickname=self.type_params['nickname'], count=get_count, page=self.page, since_id=last_id) elif self.timeline_type == "tag": raw_timeline = self.conn.statusnet_tags_timeline(tag=self.type_params['tag'], count=get_count, page=self.page, since_id=last_id) elif self.timeline_type == "sentdirect": raw_timeline = self.conn.direct_messages_sent(count=get_count, page=self.page, since_id=last_id) elif self.timeline_type == "public": raw_timeline = self.conn.statuses_public_timeline() elif self.timeline_type == "favourites": raw_timeline = self.conn.favorites(page=self.page, since_id=last_id) elif self.timeline_type == "search": raw_timeline = self.conn.search(self.type_params['query'], page=self.page, standardise=True, since_id=last_id) elif self.timeline_type == "context": raw_timeline = [] if "conversation_id" in self.type_params: # try to do it the new way raw_timeline = self.conn.statusnet_conversation(self.type_params['conversation_id'], count=get_count, since_id=last_id, page=self.page) else: if last_id == 0: # don't run this if we've already filled the timeline next_id = self.type_params['notice_id'] while next_id is not None: notice = self.conn.statuses_show(id=next_id) raw_timeline.append(notice) if "retweeted_status" in notice: next_id = notice['retweeted_status']['id'] else: next_id = notice['in_reply_to_status_id'] self.prev_page = self.page temp_timeline = [] old_ids = [n['id'] for n in self.timeline] for notice in raw_timeline: notice["ic__raw_datetime"] = helpers.normalise_datetime(notice['created_at']) notice["ic__from_web"] = True passes_filters = True if notice['id'] in old_ids: passes_filters = False continue if hasattr(config.session_store, "muted_conversations") and notice['statusnet_conversation_id'] in config.session_store.muted_conversations: passes_filters = False continue if config.config["hide_activities"] and ("source" in notice) and (notice["source"] == "activity"): passes_filters = False continue if config.config["filter_mode"] == "regex": for filter_item in config.config['filters']: if filter_item.search(notice['text']) is not None: passes_filters = False break else: for filter_item in config.config['filters']: if filter_item.lower() in notice['text'].lower(): passes_filters = False break if passes_filters: if (not self.timeline_type in ["direct", "sentdirect"]) and notice["source"] == "ostatus" and config.config['expand_remote'] and "attachments" in notice: import urllib2 for attachment in notice['attachments']: if attachment['mimetype'] != "text/html": continue req = urllib2.Request(attachment['url']) try: page = urllib2.urlopen(req).read() try: notice['text'] = helpers.html_unescape_string(helpers.title_regex.findall(page)[0]) except IndexError: # no title could be found pass except: # link was broken pass break temp_timeline.append(notice) if self.timeline_type in ["direct", "mentions"]: # alert on changes to these. maybe config option later? if (len(self.timeline) > 0) and (len(temp_timeline) > 0): # only fire when there's new stuff _and_ we've already got something in the timeline if config.config['notify'] == 'flash': curses.flash() elif config.config['notify'] == 'beep': curses.beep() if len(self.timeline) == 0: self.timeline = temp_timeline[:] else: self.timeline = temp_timeline + self.timeline if self.timeline_type != "context" and len(self.timeline) > get_count: # truncate long timelines self.timeline = self.timeline[:get_count] self.timeline.sort(key=itemgetter('id'), reverse=True) self.search_highlight_line = -1 self.update_buffer() def update_buffer(self): self.buffer.clear() maxx = self.window.getmaxyx()[1] c = 1 longest_metadata_string_len = 0 for n in self.timeline: if n["text"] is None: n["text"] = "" if "direct" in self.timeline_type: user_string = "%s -> %s" % (n["sender"]["screen_name"], n["recipient"]["screen_name"]) source_msg = "" else: atless_reply = False if "in_reply_to_screen_name" in n and n["in_reply_to_screen_name"] is not None: atless_reply = True for entity in helpers.split_entities(n["text"]): if entity["type"] == "user" and entity["text"][1:].lower() == n["in_reply_to_screen_name"].lower(): atless_reply = False break if atless_reply: if "user" in n: user_string = "%s" % (n["user"]["screen_name"]) else: user_string = "" user_string += " -> %s" % (n["in_reply_to_screen_name"]) else: if "user" in n: user_string = "%s" % (n["user"]["screen_name"]) else: user_string = "" if (n["source"] == "ostatus") and ("user" in n) and "statusnet_profile_url" in n["user"]: raw_source_msg = "from %s" % (helpers.domain_regex.findall(n["user"]["statusnet_profile_url"])[0][2]) else: raw_source_msg = "from %s" % (n["source"]) source_msg = self.html_regex.sub("", raw_source_msg) if "in_reply_to_status_id" in n and n["in_reply_to_status_id"] is not None: if not config.config["show_source"]: user_string += " +" else: source_msg += " [+]" if "retweeted_status" in n: user_string = "%s [%s's RD]" % (n["retweeted_status"]["user"]["screen_name"], n["user"]["screen_name"]) if "in_reply_to_status_id" in n["retweeted_status"]: if not config.config["show_source"]: user_string += " +" else: source_msg += " [+]" datetime_notice = helpers.normalise_datetime(n["created_at"]) time_msg = helpers.format_time(helpers.time_since(datetime_notice), short_form=True) metadata_string = time_msg + " " + user_string if config.config["show_source"]: metadata_string += " " + source_msg if len(metadata_string) > longest_metadata_string_len: longest_metadata_string_len = len(metadata_string) for n in self.timeline: if n["text"] is None: n["text"] = "" from_user = None to_user = None repeating_user = None if "direct" in self.timeline_type: from_user = n["sender"]["screen_name"] to_user = n["recipient"]["screen_name"] source_msg = "" else: if "retweeted_status" in n: repeating_user = n["user"]["screen_name"] n = n["retweeted_status"] if "user" in n: from_user = n["user"]["screen_name"] else: from_user = "" atless_reply = False if "in_reply_to_screen_name" in n and n["in_reply_to_screen_name"] is not None: atless_reply = True for entity in helpers.split_entities(n["text"]): if entity["type"] == "user" and entity["text"][1:].lower() == n["in_reply_to_screen_name"].lower(): atless_reply = False break if atless_reply: to_user = n["in_reply_to_screen_name"] if (n["source"] == "ostatus") and ("user" in n) and "statusnet_profile_url" in n["user"]: raw_source_msg = "from %s" % (helpers.domain_regex.findall(n["user"]["statusnet_profile_url"])[0][2]) else: raw_source_msg = "from %s" % (n["source"]) source_msg = self.html_regex.sub("", raw_source_msg) repeat_msg = "" if n["in_reply_to_status_id"] is not None: source_msg += " [+]" datetime_notice = helpers.normalise_datetime(n["created_at"]) time_msg = helpers.format_time(helpers.time_since(datetime_notice), short_form=True) for user in [user for user in [from_user, to_user, repeating_user] if user is not None]: if not user in config.session_store.user_cache: config.session_store.user_cache[user] = random.choice(identicurse.base_colours.items())[1] if "ic__paused_on" in n and c != 1: self.buffer.append([("-", identicurse.colour_fields["pause_line"])]) self.buffer.append([("", identicurse.colour_fields["none"])]) # Build the line line = [] if c < 10: cout = " " + str(c) else: cout = str(c) line.append((cout, identicurse.colour_fields["notice_count"])) if (c - 1) == self.chosen_one: line.append((' * ', identicurse.colour_fields["selector"])) else: line.append((' ' * 3, identicurse.colour_fields["selector"])) if config.config['compact_notices']: line.append((time_msg, identicurse.colour_fields["time"])) line.append((" ", identicurse.colour_fields["none"])) if config.config['user_rainbow']: line.append((from_user, config.session_store.user_cache[from_user])) else: line.append((from_user, identicurse.colour_fields["username"])) if from_user is not None: user_length = len(from_user) else: user_length = None if to_user is not None: line.append((" -> ", identicurse.colour_fields["none"])) if config.config['user_rainbow']: line.append((to_user, config.session_store.user_cache[to_user])) else: line.append((to_user, identicurse.colour_fields["username"])) user_length += len(" -> ") + len(to_user) if repeating_user is not None: if config.config["compact_notices"]: line.append((" [", identicurse.colour_fields["none"])) else: line.append((" [ repeat by ", identicurse.colour_fields["none"])) if config.config['user_rainbow']: line.append((repeating_user, config.session_store.user_cache[repeating_user])) else: line.append((repeating_user, identicurse.colour_fields["username"])) if config.config["compact_notices"]: line.append(("'s RD]", identicurse.colour_fields["none"])) user_length += len(" [") + len(repeating_user) + len("'s RD]") else: line.append((" ]", identicurse.colour_fields["none"])) user_length += len(" [ repeat by ") + len(repeating_user) + len(" ]") if not config.config['compact_notices']: if config.config["show_source"]: line.append((' ' * (maxx - ((len(source_msg) + len(time_msg) + user_length + (6 + len(cout))))), identicurse.colour_fields["none"])) else: line.append((' ' * (maxx - ((len(time_msg) + user_length + (5 + len(cout))))), identicurse.colour_fields["none"])) line.append((time_msg, identicurse.colour_fields["time"])) if config.config["show_source"]: line.append((' ', identicurse.colour_fields["none"])) line.append((source_msg, identicurse.colour_fields["source"])) self.buffer.append(line) line = [] else: detail_char = "" if (not config.config["show_source"]): if "in_reply_to_status_id" in n and n["in_reply_to_status_id"] is not None: detail_char = "+" elif "retweeted_status" in n: detail_char = "~" line.append((" %s" % (detail_char), identicurse.colour_fields["source"])) if config.config["show_source"]: line.append((" " + source_msg, identicurse.colour_fields["source"])) line.append((" "*((longest_metadata_string_len - (user_length + len(time_msg) + len(source_msg) + 2))), identicurse.colour_fields["none"])) else: if detail_char == "": line.append((" ", identicurse.colour_fields["none"])) line.append((" "*((longest_metadata_string_len - (user_length + len(time_msg) + 1))), identicurse.colour_fields["none"])) line.append((" | ", identicurse.colour_fields["none"])) try: notice_entities = helpers.split_entities(n['text'] or "") for entity in notice_entities: if len(entity['text']) > 0: if entity['type'] in ['user', 'group', 'tag']: entity_text_no_symbol = entity['text'][1:] cache = getattr(config.session_store, '%s_cache' % (entity['type'])) if not entity_text_no_symbol in cache: cache[entity_text_no_symbol] = random.choice(identicurse.base_colours.items())[1] if config.config['%s_rainbow' % (entity['type'])]: line.append((entity['text'], cache[entity_text_no_symbol])) else: if entity['type'] == "user": line.append((entity['text'], identicurse.colour_fields["username"])) else: line.append((entity['text'], identicurse.colour_fields[entity['type']])) else: line.append((entity['text'], identicurse.colour_fields["notice"])) self.buffer.append(line) except UnicodeDecodeError: self.buffer.append([("Caution: Terminal too shit to display this notice.", identicurse.colour_fields["warning"])]) if config.config["show_notice_links"]: line = [] base_url = helpers.base_url_regex.findall(self.conn.api_path)[0][0] if self.timeline_type in ["direct", "sentdirect"]: notice_link = "%s/message/%s" % (base_url, str(n["id"])) else: notice_link = "%s/notice/%s" % (base_url, str(n["id"])) line.append(("<%s>" % (notice_link), identicurse.colour_fields["notice_link"])) self.buffer.append(line) if not config.config['compact_notices']: self.buffer.append([]) c += 1 class Profile(Tab): def __init__(self, conn, window, id): self.conn = conn self.id = id self.name = "Profile (%s)" % self.id self.fields = [ # display name, internal field name, skip a line after this field? ("Real Name", "name", True), ("Bio", "description", False), ("Location", "location", False), ("URL", "url", False), ("User ID", "id", False), ("Joined at", "created_at", True), ("Followed by", "followers_count", False), ("Following", "friends_count", False), ("Followed by you", "following", True), ("Favourites", "favourites_count", False), ("Notices", "statuses_count", False), ("Average daily notices", "notices_per_day", True) ] Tab.__init__(self, window) def update(self): try: self.profile = self.conn.users_show(screen_name=self.id) # numerical fields, convert them to strings to make the buffer code more clean for field in ['id', 'created_at', 'followers_count', 'friends_count', 'favourites_count', 'statuses_count']: self.profile[field] = str(self.profile[field]) # special handling for following if self.profile['following']: self.profile['following'] = "Yes" else: self.profile['following'] = "No" # create this field specially datetime_joined = helpers.normalise_datetime(self.profile['created_at']) days_since_join = helpers.single_unit(helpers.time_since(datetime_joined), "days")['days'] self.profile['notices_per_day'] = "%0.2f" % (float(self.profile['statuses_count']) / days_since_join) except StatusNetError, e: if e.errcode == 404: self.profile = None self.update_buffer() def update_buffer(self): self.buffer.clear() if self.profile is not None: self.buffer.append([("@" + self.profile['screen_name'] + "'s Profile", identicurse.colour_fields['profile_title'])]) self.buffer.append([("", identicurse.colour_fields['none'])]) for field in self.fields: if self.profile[field[1]] is not None: line = [] line.append((field[0] + ":", identicurse.colour_fields['profile_fields'])) line.append((" ", identicurse.colour_fields['none'])) line.append((self.profile[field[1]], identicurse.colour_fields['profile_values'])) self.buffer.append(line) if field[2]: self.buffer.append([("", identicurse.colour_fields['none'])]) else: self.buffer.append([("There is no user called @%s on this instance." % (self.id), identicurse.colour_fields['none'])]) identicurse-0.9+dfsg0/src/identicurse/helpers.py0000644000175000017500000002357211720536557021040 0ustar mvdanmvdan# -*- coding: utf-8 -*- # # Copyright (C) 2010-2012 Reality and Psychedelic Squid # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import time, datetime, htmlentitydefs, re, urllib, urllib2, locale, os, platform, sys, string DATETIME_FORMAT = "%a %b %d %H:%M:%S +0000 %Y" offset_regex = re.compile("[+-][0-9]{4}") base_url_regex = re.compile("(http(s|)://.+?)/.*") title_regex = re.compile("\(.*)\<\/title\>") ur1_regex = re.compile("Your ur1 is: (http://ur1\.ca/[0-9A-Za-z]+)") url_regex = re.compile("(?:ht|f)tp[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+") domain_regex = re.compile("http(s|)://(www\.|)(.+?)(/.*|)$") def normalise_datetime(timestring): locale.setlocale(locale.LC_TIME, 'C') # hacky fix because statusnet uses english timestrings regardless of locale datetime_no_offset = offset_regex.sub("+0000", timestring) attempts = 10 while attempts > 0: attempts -= 1 try: normalised_datetime = datetime.datetime.strptime(datetime_no_offset, DATETIME_FORMAT) + utc_offset(timestring) break except ValueError: # something else changed the locale, and Python threw a hissy fit pass locale.setlocale(locale.LC_TIME, '') # other half of the hacky fix return normalised_datetime def time_since(datetime_then): if datetime_then > datetime.datetime.utcnow(): return {'days':0, 'hours':0, 'minutes':0, 'seconds':0} time_diff_raw = datetime.datetime.utcnow() - datetime_then days_since = time_diff_raw.days seconds_since = time_diff_raw.seconds time_diff = {} time_diff['days'] = int(round(days_since)) time_diff['hours'] = int(round(seconds_since / (60 * 60))) seconds_since -= time_diff['hours'] * (60 * 60) time_diff['minutes'] = int(round(seconds_since / 60)) seconds_since -= time_diff['minutes'] * 60 time_diff['seconds'] = int(round(seconds_since)) return time_diff def format_time(time_dict, floating=False, short_form=False): timestr = "" if short_form: formatstr = "%d%s " else: if floating: formatstr = "%0.1f %s " else: formatstr = "%d %s " if short_form: if time_dict['days'] > 0: if time_dict['hours'] >= 12: time_dict['days'] += 1 if (time_dict['hours'] != 0) or (time_dict['minutes'] != 0) or (time_dict['seconds'] != 0): # timestr = "~" time_dict['hours'], time_dict['minutes'], time_dict['seconds'] = 0, 0, 0 elif time_dict['hours'] > 0: if time_dict['minutes'] >= 30: time_dict['hours'] += 1 if (time_dict['minutes'] != 0) or (time_dict['seconds'] != 0): # timestr = "~" time_dict['minutes'], time_dict['seconds'] = 0, 0 elif time_dict['minutes'] > 0: if time_dict['seconds'] >= 30: time_dict['minutes'] += 1 if time_dict['seconds'] != 0: # timestr = "~" time_dict['seconds'] = 0 for unit in ['days', 'hours', 'minutes', 'seconds']: if short_form: if time_dict[unit] > 0: timestr += formatstr % (time_dict[unit], unit[0]) else: if time_dict[unit] > 1: timestr += formatstr % (time_dict[unit], unit) elif time_dict[unit] == 1: timestr += formatstr % (time_dict[unit], unit[:-1]) if timestr == "": timestr = "Now" else: timestr += "ago" return timestr def single_unit(time_dict, unit): total_seconds = float(time_dict['seconds']) total_seconds += (time_dict['minutes'] * 60) total_seconds += (time_dict['hours'] * (60 * 60)) total_seconds += (time_dict['days'] * (60 * 60 * 24)) time_dict = {'days':0, 'hours':0, 'minutes':0, 'seconds':0} if unit == "seconds": time_dict['seconds'] = total_seconds elif unit == "minutes": time_dict['minutes'] = (total_seconds / 60) elif unit == "hours": time_dict['hours'] = (total_seconds / (60 * 60)) elif unit == "days": time_dict['days'] = (total_seconds / (60 * 60 * 24)) return time_dict def utc_offset(time_string): offset = offset_regex.findall(time_string)[0] offset_hours = int(offset[1:3]) offset_minutes = int(offset[3:]) return datetime.timedelta(hours=offset_hours,minutes=offset_minutes) def find_split_point(text, width): split_point = width - 1 while True: if split_point == 0: # no smart split point was found, split unsmartly split_point = width - 1 break elif split_point < 0: split_point = 0 break if text[split_point-1] == " ": break else: split_point -= 1 return split_point def html_unescape_block(block): block_text = block.group(0) block_codepoint = None if block_text[:2] == "&#": # codepoint try: if block_text[2] == "x": # hexadecimal codepoint block_codepoint = int(block_text[3:-1], 16) else: # decimal codepoint block_codepoint = int(block_text[2:-1]) except ValueError: # codepoint was mangled/invalid, don't bother trying to interpret it pass else: # named character try: block_codepoint = htmlentitydefs.name2codepoint[block_text[1:-1]] except KeyError: # name was invalid, don't try to interpret pass if block_codepoint is not None: return unichr(block_codepoint) else: return block_text def html_unescape_string(escaped_string): return re.sub("&#?\w+;", html_unescape_block, escaped_string) def find_longest_common_start(words): if len(words) == 0: return "" last_match = "" for length in xrange(len(words[0]) + 1): match_string = words[0][:length] match = True for word in words: if word[:length] != match_string: match = False if not match: break last_match = match_string return last_match def find_fuzzy_matches(fragment, words): if len(fragment) == 0: return [] matches = [] for word in words: fragment_index = 0 for char in word: if char == fragment[fragment_index]: fragment_index += 1 if fragment_index == len(fragment): # all chars existed in order, this is a fuzzy match matches.append(word) break return matches def ur1ca_shorten(longurl): request = urllib2.Request("http://ur1.ca/", urllib.urlencode({'longurl':longurl})) response = urllib2.urlopen(request) page = response.read() results = ur1_regex.findall(page) if len(results) > 0: return results[0] else: # something went wrong, return the original url return longurl def set_terminal_title(title_text): if platform.system() != "Windows": sys.stdout.write("\x1b]0;" + title_text + "\x07") # set the title the unix-y way else: os.system("title " + title_text) # do it the windows-y way def split_entities(raw_notice_text): entities = [{"text":"", "type":"plaintext"}] raw_notice_text = " " + raw_notice_text + " " char_index = 0 while char_index < len(raw_notice_text): if entities[-1]['type'] != "plaintext" and not raw_notice_text[char_index].isalnum() and not raw_notice_text[char_index] in [".", "_", "-"]: next_entity_text = "" for i in xrange(len(entities[-1]['text'])): if len(entities[-1]['text']) > 1 and entities[-1]['text'][-1] in [".", "-"]: next_entity_text += entities[-1]['text'][-1] entities[-1]['text'] = entities[-1]['text'][:-1] else: break entities.append ({"text":next_entity_text, "type":"plaintext"}) if (raw_notice_text[char_index] in string.whitespace or raw_notice_text[char_index] in string.punctuation) and char_index < (len(raw_notice_text) - 2): entities[-1]['text'] += raw_notice_text[char_index] char_index += 1 if raw_notice_text[char_index] in ["@", "!", "#"] and (raw_notice_text[char_index+1].isalnum() or (raw_notice_text[char_index+1] in [".", "_", "-"])) and (not raw_notice_text[char_index-1] in ["@", "!", "#", ".", "_", "-"]) and (not raw_notice_text[char_index-1].isalnum()): if raw_notice_text[char_index] == "@": entities.append({"text":"@", "type":"user"}) elif raw_notice_text[char_index] == "!": entities.append({"text":"!", "type":"group"}) elif raw_notice_text[char_index] == "#": entities.append({"text":"#", "type":"tag"}) char_index += 1 else: entities[-1]['text'] += raw_notice_text[char_index] char_index += 1 # strip the extra space that was prepended if entities[0]['text'] == " ": entities = entities[1:] else: entities[0]['text'] = entities[0]['text'][1:] # and the one that was appended if entities[-1]['text'] == " ": entities = entities[:-1] else: entities[-1]['text'] = entities[-1]['text'][:-1] return entities identicurse-0.9+dfsg0/src/identicurse/__init__.py0000644000175000017500000001235711720536557021134 0ustar mvdanmvdan# -*- coding: utf-8 -*- # # Copyright (C) 2010-2012 Reality and Psychedelic Squid # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ Initial module for IdentiCurse. Parses options and displays slogan, then hands off control to the main identicurse.py module. """ from identicurse import IdentiCurse from optparse import OptionParser import random, os PRESET_SLOGANS = [ "100% hippy-approved", "powered by hatred", "we get OAuth now", "don't drink and dent", "@psquid can't spell hippy", "Stupid sexy Flanders", "curry in the i-webs", "Got GNOME git commit access", "YOUR SOUL TO THE HOMOSEXUAL AGENDA", "Bullshit Bingo", "trying to do teh frees", "coming and coming and coming and coming", "Do you store your passwords in the back yard?", "Let's neuter this bullshit!", "it's probably just recycled bullshit.", "I'm on a rampage and kill everyone.", "a Tragedie in three parts.", "#metamicroblogging", "#metametamicroblogging", "EXCEPT IN NEBRASKA", "ATTENTION SNOW: GTFO ITS TOO WARM FOR YOU!", "eating paracetamol sandwiches.", "Do not operate heavy machinery while using IdentiCurse.", "44% the same as bathing in fine grape juice.", "DOT MATRIX WITH STEREO SOUND", "oh god how did this get here I am not good with computer", "Pregnant women and those with heart conditions are advised against using this software.", "because some vpn won't run with the cool friends", "Making \"git pull\" fun again, since 2010.", "like a compact disc to the head!", "along with his mechanical ass-kicking leg.", "TIME FOR GROUP HUG.", "GLORIOUS VICTORY", "the sock ruse was a... DISTACTION", "wigeons have my car keys.", "life is like a zombie in my head.", "head to the nearest ENTRANCE and immediately call YOUR MUTANT FIRE DANCING MOON POSSE", "it's got what dents crave.", "it has lightsabers.", "enemy of #scannability.", "T-rex rules the school.", "GET BLUE SPHERES", "not affiliated with @sandersch's nipple.", "we're sitti. D next to toy fish", "do the federation!", "some BAD SOFTWARE with regrettable REALNESS ATTRIBUTE", "as seen on RealiTV!", "IDENTICURSE DISLIKES SMOKE", "we got a gorilla for sale, Magilla gorilla for sale.", "but Cuba", ] def main(): """ Innit. """ parser = OptionParser() parser.add_option("-c", "--config", help="specify an alternative config dir to use", action="store", type="string", dest="config_dirname", metavar="FILE") parser.add_option("-s", "--slogans", help="specify an alternative slogans file to use", action="store", type="string", dest="slogans_filename", metavar="FILE") parser.add_option("--colour-check", help="check if colour support is available, and if so, how many colours", action="store_true", dest="colour_check") options = parser.parse_args()[0] if (options.colour_check is not None) and (options.colour_check == True): colour_check() return additional_config = {} if options.slogans_filename is not None: user_slogans_file = os.path.expanduser(options.slogans_filename) else: user_slogans_file = os.path.join(os.path.expanduser("~"), ".identicurse_slogans") if options.config_dirname is not None: additional_config['config_dirname'] = options.config_dirname try: user_slogans_raw = open(user_slogans_file).read() user_slogans = [slogan for slogan in user_slogans_raw.split("\n") if slogan.strip() != ""] slogans = user_slogans except IOError: slogans = PRESET_SLOGANS additional_config['slogans'] = slogans print "Welcome to IdentiCurse 0.9 (Inverkeilor) - %s" % (random.choice(slogans)) IdentiCurse(additional_config) def colour_check(): """ Display brief message informing user how many colours their system's curses library reports as available. """ import curses curses.initscr() if curses.has_colors(): curses.start_color() curses.use_default_colors() msg = "System curses library reports that in your current system state, it supports %d colours. For many terminals, adding \"export TERM=xterm-256color\" to your startup scripts will make far more colours available to curses." % (curses.COLORS) else: msg = "System curses library reports that (at least in your current system state) colour support is not available." curses.endwin() print msg if __name__ == '__main__': main() identicurse-0.9+dfsg0/src/identicurse/identicurse.py0000644000175000017500000026573511720536557021725 0ustar mvdanmvdan# -*- coding: utf-8 -*- # # Copyright (C) 2010-2012 Reality and Psychedelic Squid # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os, sys, curses, locale, re, subprocess, random, platform try: import json except ImportError: import simplejson as json from threading import Timer from textbox import Textbox import urllib2 from statusnet import StatusNet, StatusNetError from tabbage import * from statusbar import StatusBar from tabbar import TabBar import config import helpers locale.setlocale(locale.LC_ALL, '') code = locale.getpreferredencoding() colour_fields = { "none": 0, "statusbar": 1, "timelines": 2, "selector": 4, "username": 5, "time": 6, "source": 7, "notice_count": 8, "notice": 9, "profile_title": 10, "profile_fields": 11, "profile_values": 12, "group": 13, "tag": 14, "search_highlight": 15, "tabbar": 16, "tabbar_active": 17, "notice_link": 18, "warning": 19, "pause_line": 20, } if platform.system() == "Windows": # Handle Windows' colour-order fuckery. This is only true if we are running on pure Windows. If we're on Cygwin, which handles colours correctly anyway, this won't match. colours = { "none": -1, "black": 0, "blue": 1, "green": 2, "cyan": 3, "red": 4, "magenta": 5, "brown": 6, "white": 7, "grey": 8, "light_blue": 9, "light_green": 10, "light_cyan": 11, "light_red": 12, "light_magenta": 13, "yellow": 14, "light_white": 15 } else: colours = { "none": -1, "black": 0, "red": 1, "green": 2, "brown": 3, "blue": 4, "magenta": 5, "cyan": 6, "white": 7, "grey": 8, "light_red": 9, "light_green": 10, "yellow": 11, "light_blue": 12, "light_magenta": 13, "light_cyan": 14, "light_white": 15 } base_colours = {} oauth_consumer_keys = { "identi.ca": "d4f54e34af11ff8d35b79b7557ad771c", } oauth_consumer_secrets = { "identi.ca": "8fb75c0a9bbca78fe0e85acc62a9169c", } class IdentiCurse(object): """Contains Main IdentiCurse application""" def __init__(self, additional_config={}): helpers.set_terminal_title("IdentiCurse") if hasattr(sys, "frozen"): # if this matches, we're running under py2exe, so we need to do some magic to get the correct path executable_path = sys.executable else: executable_path = __file__ self.path = os.path.dirname(os.path.realpath(unicode(executable_path, sys.getfilesystemencoding()))) self.qreply = False config.session_store.slogans = additional_config["slogans"] if "config_dirname" in additional_config: config.config.basedir = os.path.expanduser(additional_config['config_dirname']) else: config.config.basedir = os.path.join(os.path.expanduser("~") ,".identicurse") config.config.filename = os.path.join(config.config.basedir, "config.json") config.config.auth_filename = os.path.join(config.config.basedir, "auth.json") if os.path.exists(config.config.basedir) and not os.path.isdir(config.config.basedir): # (if a .identicurse file, as used by <= 0.7.x, exists) config_temp = open(config.config.basedir, "r").read() os.remove(config.config.basedir) os.mkdir(config.config.basedir) open(config.config.filename, "w").write(config_temp) if not os.path.exists(config.config.basedir): os.mkdir(config.config.basedir) if os.path.exists(config.config.filename) and not os.path.exists(config.config.auth_filename): unclean_config = json.loads(open(config.config.filename, "r").read()) clean_config = {} auth_config = {} for key, value in unclean_config.items(): if key in config.auth_fields: auth_config[key] = value else: clean_config[key] = value open(config.config.filename, "w").write(json.dumps(clean_config, indent=4)) open(config.config.auth_filename, "w").write(json.dumps(auth_config, indent=4)) try: if os.path.exists(config.config.filename) or os.path.exists(os.path.join("/etc", "identicurse.conf")): if not config.config.load(): config.config.load(os.path.join("/etc", "identicurse.conf")) else: import getpass, time # no config yet, so let's build one config.config.load(os.path.join(self.path, "config.json")) print "No config was found, so we will now run through a few quick questions to set up a basic config for you (which will be saved as %s so you can manually edit it later). If the default (where defaults are available, they're stated in []) is already fine for any question, just press Enter without typing anything, and the default will be used." % (config.config.filename) print "This version of IdentiCurse supports OAuth login. Using OAuth to log in means that you do not need to enter your username and password." use_oauth = raw_input("Use OAuth [Y/n]? ").upper() if use_oauth == "": use_oauth = "Y" if use_oauth[0] == "Y": config.config['use_oauth'] = True else: config.config['use_oauth'] = False if not config.config['use_oauth']: config.config['username'] = raw_input("Username: ") config.config['password'] = getpass.getpass("Password: ") api_path = raw_input("API path [%s]: " % (config.config['api_path'])) if api_path != "": if len(api_path) < 7 or api_path[:7] != "http://" and api_path[:8] != "https://": api_path = "http://" + api_path if len(api_path) >= 7 and api_path[:5] != "https": https_api_path = "https" + api_path[4:] response = raw_input("You have not used an https URL. This means everything you do with IdentiCurse will travel over your connection _unencrypted_. Would you rather use '%s' as your API path [Y/n]? " % (https_api_path)).upper() if response == "": response = "Y" if response[0] == "Y": api_path = https_api_path config.config['api_path'] = api_path update_interval = raw_input("Auto-refresh interval (in whole seconds) [%d]: " % (config.config['update_interval'])) if update_interval != "": try: config.config['update_interval'] = int(update_interval) except ValueError: print "Sorry, you entered an invalid interval. The default of %d will be used instead." % (config.config['update_interval']) notice_limit = raw_input("Number of notices to fetch per timeline page [%d]: " % (config.config['notice_limit'])) if notice_limit != "": try: config.config['notice_limit'] = int(notice_limit) except ValueError: print "Sorry, you entered an invalid number of notices. The default of %d will be used instead." % (config.config['notice_limit']) # try: if config.config['use_oauth']: instance = helpers.domain_regex.findall(config.config['api_path'])[0][2] if not instance in oauth_consumer_keys: print "No suitable consumer keys stored locally, fetching latest list..." req = urllib2.Request("http://identicurse.net/api_keys.json") resp = urllib2.urlopen(req) api_keys = json.loads(resp.read()) if not instance in api_keys['keys']: sys.exit("Sorry, IdentiCurse currently lacks the API keys needed to support OAuth with your instance (%(instance)s). If %(instance)s is a public instance, let us know which one it is, and we'll add support as soon as possible." % (locals())) else: temp_conn = StatusNet(config.config['api_path'], auth_type="oauth", consumer_key=api_keys['keys'][instance], consumer_secret=api_keys['secrets'][instance], save_oauth_credentials=config.store_oauth_keys) config.config["consumer_key"] = api_keys['keys'][instance] config.config["consumer_secret"] = api_keys['secrets'][instance] else: temp_conn = StatusNet(config.config['api_path'], auth_type="oauth", consumer_key=oauth_consumer_keys[instance], consumer_secret=oauth_consumer_secrets[instance], save_oauth_credentials=config.store_oauth_keys) else: temp_conn = StatusNet(config.config['api_path'], config.config['username'], config.config['password']) # except Exception, (errmsg): # sys.exit("Couldn't establish connection: %s" % (errmsg)) print "Okay! Everything seems good! Your new config will now be saved, then IdentiCurse will start properly." config.config.save() except ValueError, e: sys.exit("ERROR: Your config file could not be succesfully loaded due to JSON syntax error(s). Please fix it.\nOriginal error: %s" % (str(e))) self.last_page_search = {'query':"", 'occurs':[], 'viewing':0, 'tab':-1} # prepare the known commands list self.known_commands = [ "/reply", "/favourite", "/repeat", "/direct", "/delete", "/profile", "/spamreport", "/block", "/unblock", "/user", "/context", "/subscribe", "/unsubscribe", "/group", "/groupjoin", "/groupleave", "/groupmember", "/tag", "/sentdirects", "/favourites", "/search", "/home", "/mentions", "/directs", "/public", "/config", "/alias", "/link", "/bugreport", "/featurerequest", "/quote", "/quit", "/mute", "/unmute", ] # load all known commands and aliases into the command list config.session_store.commands = [command[1:] for command in self.known_commands] + [alias[1:] for alias in config.config["aliases"]] # Set some defaults for configs that we will always need to use, but that are optional if not "enable_colours" in config.config: config.config["enable_colours"] = True if config.config["enable_colours"]: default_colour_scheme = { "timelines": ("none", "none"), "statusbar": ("black", "white"), "tabbar": ("white", "blue"), "tabbar_active": ("blue", "white"), "selector": ("brown", "none"), "time": ("brown", "none"), "source": ("green", "none"), "notice": ("none", "none"), "notice_count": ("blue", "none"), "username": ("cyan", "none"), "group": ("cyan", "none"), "tag": ("cyan", "none"), "profile_title": ("cyan", "none"), "profile_fields": ("blue", "none"), "profile_values": ("none", "none"), "search_highlight": ("white", "blue"), "notice_link": ("green", "none"), "warning": ("black", "red"), "pause_line": ("white", "red"), "none": ("none", "none") } # Default colour scheme if not "colours" in config.config: config.config["colours"] = default_colour_scheme else: for part in colour_fields: if not part in config.config["colours"]: config.config["colours"][part] = default_colour_scheme[part] if not "search_case_sensitive" in config.config: config.config['search_case_sensitive'] = "sensitive" if not "notify" in config.config: config.config['notify'] = "flash" if not "long_dent" in config.config: config.config['long_dent'] = "split" if not "filters" in config.config: config.config['filters'] = [] if (not "filter_mode" in config.config) or (not config.config["filter_mode"] in ["plain", "regex"]): config.config['filter_mode'] = "plain" if not "notice_limit" in config.config: config.config['notice_limit'] = 25 if not "browser" in config.config: config.config['browser'] = "xdg-open '%s'" if not "border" in config.config: config.config['border'] = False if not "compact_notices" in config.config: config.config['compact_notices'] = False if not "user_rainbow" in config.config: config.config["user_rainbow"] = False if not "group_rainbow" in config.config: config.config["group_rainbow"] = False if not "tag_rainbow" in config.config: config.config["tag_rainbow"] = False if not "expand_remote" in config.config: config.config["expand_remote"] = False if not "smooth_cscroll" in config.config: config.config["smooth_cscroll"] = True if not "use_oauth" in config.config: config.config["use_oauth"] = False if not "username" in config.config: config.config["username"] = "" if not "password" in config.config: config.config["password"] = "" if not "show_notice_links" in config.config: config.config["show_notice_links"] = False if not "length_override" in config.config: config.config["length_override"] = 0 if not "prefill_user_cache" in config.config: config.config["prefill_user_cache"] = False if not "show_source" in config.config: config.config["show_source"] = True if (not "tab_complete_mode" in config.config) or (not config.config["tab_complete_mode"] in ["exact", "fuzzy"]): config.config["tab_complete_mode"] = "exact" if not "hide_activities" in config.config: config.config["hide_activities"] = False if not "new_reply_mode" in config.config: config.config["new_reply_mode"] = False if not "status_slogans" in config.config: config.config["status_slogans"] = True if not "enumerate_tabs" in config.config: config.config["enumerate_tabs"] = True if not "keys" in config.config: config.config['keys'] = {} if not "ui_order" in config.config: config.config['ui_order'] = ["divider", "entry", "divider", "notices", "statusbar", "tabbar"] # this will recreate the same layout as the old UI for ui_item in ["entry", "notices", "statusbar", "tabbar"]: # ensure no UI element is ommitted by appending any missing ones to the end if not ui_item in config.config['ui_order']: config.config['ui_order'].append(ui_item) while config.config['ui_order'].count(ui_item) > 1: # if item listed more than once, remove all but the last occurence config.config['ui_order'].remove(ui_item) if config.config["filter_mode"] == "regex": raw_filters = config.config["filters"] config.config["filters"] = [] for raw_filter in raw_filters: config.config["filters"].append(re.compile(raw_filter)) keybind_actions = ("firstpage", "newerpage", "olderpage", "refresh", "input", "commandinput", "search", "quit", "closetab", "help", "nexttab", "prevtab", "qreply", "creply", "cfav", "cunfav", "ccontext", "crepeat", "cnext", "cprev", "cfirst", "clast", "nextmatch", "prevmatch", "creplymode", "cquote", "tabswapleft", "tabswapright", "cdelete", "pausetoggle", "pausetoggleall", "scrollup", "scrolltop", "pageup", "pagedown", "scrolldown", "scrollbottom", "togglenoticelinks", "nexttabcycle", "prevtabcycle", "mute", "unmute") default_keys = { "nexttab": [">"], "nexttabcycle": ["\t", "+"], "prevtab": ["<"], "prevtabcycle": [curses.KEY_BTAB, "-"], "tabswapright": ["."], "tabswapleft": [","], "scrollup": [curses.KEY_UP, "k"], "scrolltop": [curses.KEY_HOME, "g"], "pageup": [curses.KEY_PPAGE, "b"], "scrolldown": [curses.KEY_DOWN, "j"], "scrollbottom": [curses.KEY_END, "G"], "pagedown": [curses.KEY_NPAGE, " "], "firstpage": ["="], "newerpage": [curses.KEY_LEFT], "olderpage": [curses.KEY_RIGHT], "refresh": ["r"], "input": ["i"], "commandinput": [":"], "search": ["/"], "quit": ["q"], "closetab": ["x"], "help": ["h"], "qreply": ["l"], "nextmatch": ["n"], "prevmatch": ["N"], "cdelete": ["#"], "pausetoggleall": ["P"], "creply": ["D"], "creplymode": ["d"], "cnext": ["s"], "cprev": ["a"], "cfirst": ["z"], "clast": ["Z"], "cfav": ["f"], "cunfav": ["F"], "crepeat": ["e"], "cquote": ["E"], "ccontext": ["c"], "pausetoggle": ["p"], "togglenoticelinks": ["L"], "mute": ["m"], "unmute": ["M"], } self.keybindings = {} assigned_keys = [] for action in keybind_actions: self.keybindings[action] = [] if action in config.config['keys']: for key in config.config['keys'][action]: if isinstance(key, basestring): key = ord(key) self.keybindings[action].append(key) assigned_keys.append(key) for action in keybind_actions: if action in default_keys: for key in default_keys[action]: if isinstance(key, basestring): key, orig_key = ord(key), key if not key in assigned_keys: self.keybindings[action].append(key) elif len(self.keybindings) == 0: print "WARNING: Tried to assign action '%(action)s' to key '%(key)s', but a user-set keybinding already uses '%(key)s'. This will leave '%(action)s' with no keybindings, so make sure to add a custom binding for '%(action)s' if you still want to use it." % {'action': action, 'key': orig_key} try: if config.config["use_oauth"]: instance = helpers.domain_regex.findall(config.config['api_path'])[0][2] if "consumer_key" in config.config: self.conn = StatusNet(config.config['api_path'], auth_type="oauth", consumer_key=config.config["consumer_key"], consumer_secret=config.config["consumer_secret"], oauth_token=config.config["oauth_token"], oauth_token_secret=config.config["oauth_token_secret"], save_oauth_credentials=config.store_oauth_keys) elif not instance in oauth_consumer_keys: print "No suitable consumer keys stored locally, fetching latest list..." req = urllib2.Request("http://identicurse.net/api_keys.json") resp = urllib2.urlopen(req) api_keys = json.loads(resp.read()) if not instance in api_keys['keys']: sys.exit("Sorry, IdentiCurse currently lacks the API keys needed to support OAuth with your instance (%(instance)s). If %(instance)s is a public instance, let us know which one it is (filing a bug at http://bugzilla.identicurse.net/ is the preferred way of doing so), and we'll add support as soon as possible." % (locals())) else: self.conn = StatusNet(config.config['api_path'], auth_type="oauth", consumer_key=api_keys['keys'][instance], consumer_secret=api_keys['secrets'][instance], oauth_token=config.config["oauth_token"], oauth_token_secret=config.config["oauth_token_secret"], save_oauth_credentials=config.store_oauth_keys) config.config["consumer_key"] = api_keys['keys'][instance] config.config["consumer_secret"] = api_keys['secrets'][instance] config.config.save() else: self.conn = StatusNet(config.config['api_path'], auth_type="oauth", consumer_key=oauth_consumer_keys[instance], consumer_secret=oauth_consumer_secrets[instance], oauth_token=config.config["oauth_token"], oauth_token_secret=config.config["oauth_token_secret"], save_oauth_credentials=config.store_oauth_keys) else: self.conn = StatusNet(config.config['api_path'], config.config['username'], config.config['password']) except Exception, (errmsg): sys.exit("ERROR: Couldn't establish connection: %s" % (errmsg)) if config.config["prefill_user_cache"]: print "Prefilling the user cache based on your followed users. This will take a little while, especially on slower connections. Please be patient." users = [] for user_profile in self.conn.statuses_friends(): screen_name = user_profile["screen_name"] if not screen_name in users: users.append(screen_name) for user in users: if not hasattr(config.session_store, "user_cache"): config.session_store.user_cache = {} config.session_store.user_cache[user] = random.choice(range(8)) self.insert_mode = False self.search_mode = False self.quote_mode = False self.reply_mode = False curses.wrapper(self.initialise) def redraw(self): self.screen.clear() self.screen.refresh() self.y, self.x = self.screen.getmaxyx() if config.config['border']: if self.screen.getmaxyx() == (self.y, self.x): self.main_window = self.screen.subwin(self.y-3, self.x-3, 2, 2) else: return self.redraw() self.main_window.box(0, 0) else: if self.screen.getmaxyx() == (self.y, self.x): self.main_window = self.screen.subwin(self.y-1, self.x-1, 1, 1) else: return self.redraw() self.main_window.keypad(1) y, x = self.main_window.getmaxyx() current_y = 0 if config.config['border']: current_y += 3 y -= 3 if self.conn.length_limit == 0 and config.config["length_override"] != 0: entry_lines = 3 else: if config.config["length_override"] != 0: notice_length = config.config["length_override"] else: notice_length = self.conn.length_limit entry_lines = (notice_length / x) + 1 if entry_lines > (y / 2): # if entry box would take more than 1/2 of the screen height entry_lines = y / 2 for part in config.config['ui_order']: if part == "divider": current_y += 1 elif part == "entry": if config.config['border']: if self.screen.getmaxyx() == (self.y, self.x): self.entry_window = self.main_window.subwin(entry_lines, x-6, current_y, 5) current_y += entry_lines else: return self.redraw() else: if self.screen.getmaxyx() == (self.y, self.x): self.entry_window = self.main_window.subwin(entry_lines, x-2, current_y, 1) current_y += entry_lines else: return self.redraw() self.text_entry = Textbox(self.entry_window, self.validate, insert_mode=True) self.text_entry.stripspaces = 1 elif part == "notices": if config.config['border']: if self.screen.getmaxyx() == (self.y, self.x): self.notice_window = self.main_window.subwin(y-(entry_lines + 1 + config.config['ui_order'].count("divider")), x-4, current_y, 5) current_y += y - (entry_lines + 1 + config.config['ui_order'].count("divider")) else: return self.redraw() else: if self.screen.getmaxyx() == (self.y, self.x): self.notice_window = self.main_window.subwin(y-(entry_lines + 1 + config.config['ui_order'].count("divider")), x, current_y, 1) current_y += y - (entry_lines + 1 + config.config['ui_order'].count("divider")) else: return self.redraw() # I don't like this, but it looks like it has to be done if hasattr(self, 'tabs'): for tab in self.tabs: tab.window = self.notice_window elif part == "statusbar": if config.config['border']: if self.screen.getmaxyx() == (self.y, self.x): self.status_window = self.main_window.subwin(1, x-5, current_y, 5) current_y += 1 else: return self.redraw() else: if self.screen.getmaxyx() == (self.y, self.x): self.status_window = self.main_window.subwin(1, x, current_y, 1) current_y += 1 else: return self.redraw() elif part == "tabbar": if config.config['border']: if self.screen.getmaxyx() == (self.y, self.x): self.tab_bar_window = self.main_window.subwin(1, x-5, current_y, 5) current_y += 1 else: return self.redraw() else: if self.screen.getmaxyx() == (self.y, self.x): self.tab_bar_window = self.main_window.subwin(1, x, current_y, 1) current_y += 1 else: return self.redraw() if hasattr(self, 'status_bar'): self.status_bar.window = self.status_window self.status_bar.redraw() if hasattr(self, 'tab_bar'): self.tab_bar.window = self.tab_bar_window self.screen.bkgd(" ", curses.color_pair(colour_fields["none"])) self.main_window.bkgd(" ", curses.color_pair(colour_fields["none"])) self.notice_window.bkgd(" ", curses.color_pair(colour_fields["timelines"])) self.status_window.bkgd(" ", curses.color_pair(colour_fields["statusbar"])) self.tab_bar_window.bkgd(" ", curses.color_pair(colour_fields["tabbar"])) self.screen.refresh() def initialise(self, screen): self.screen = screen try: curses.curs_set(0) # try to hide the cursor. Textbox makes it visible again, then hides it on exit except: pass curses.noecho() curses.cbreak() curses.use_default_colors() if curses.has_colors() and config.config['enable_colours'] == True: curses.start_color() if "custom_colours" in config.config: temp_colours = colours.copy() temp_colours.update(config.config['custom_colours']) if not curses.can_change_color(): raise Exception("Cannot set custom colours, since your terminal does not support changing colour values. Using \"export TERM=xterm-256color\" may resolve this, since some terminals only enable that function when 256 colours are available.") elif len(temp_colours) >= curses.COLORS: raise Exception("Cannot set custom colours, since your terminal supports only %d colour slots. Adding all the custom colours defined in your config would need %d slots. For many terminals, using \"export TERM=xterm-256color\" will allow use of 256 slots." % (curses.COLORS, len(temp_colours))) else: colour_num = len(colours) for colour_name, colour_value in config.config['custom_colours'].items(): if colour_value[0] == "#": colour_value = colour_value[1:] r = int((ord(colour_value[0:2].decode("hex")) * 1000.0) / 255.0) g = int((ord(colour_value[2:4].decode("hex")) * 1000.0) / 255.0) b = int((ord(colour_value[4:6].decode("hex")) * 1000.0) / 255.0) if colour_name in colours: # if we're redefining an already existing colour curses.init_color(colours[colour_name], r, g, b) else: curses.init_color(colour_num, r, g, b) colours[colour_name] = colour_num colour_num += 1 for field, (fg, bg) in config.config['colours'].items(): try: curses.init_pair(colour_fields[field], colours[fg], colours[bg]) except: continue c = 50 for (key, value) in colours.items(): if (value + 1) > curses.COLORS: continue if not key in ("black", "white", "none") and key != config.config['colours']['notice']: base_colours[colours[key]] = c curses.init_pair(c, value, colours["none"]) c += 1 else: for field in colour_fields: curses.init_pair(colour_fields[field], -1, -1) c = 50 for (key, value) in colours.items(): if key != "black": base_colours[colours[key]] = c curses.init_pair(c, -1, -1) c += 1 self.redraw() self.status_bar = StatusBar(self.status_window) self.status_bar.update("Welcome to IdentiCurse") self.tabs = [] for tabspec in config.config['initial_tabs'].split("|"): if tabspec[0] == "@": tabspec = "user:" + tabspec[1:] elif tabspec[0] == "!": tabspec = "group:" + tabspec[1:] elif tabspec[0] == "#": tabspec = "tag:" + tabspec[1:] elif tabspec[0] == "?": tabspec = "search:" + tabspec[1:] tab = tabspec.split(':') if tab[0] in ("home", "mentions", "direct", "public", "sentdirect", "favourites"): already_have_one = False for tab_obj in self.tabs: # awkward name, but we already have a tab variable if hasattr(tab_obj, 'timeline_type'): if tab_obj.timeline_type == tab[0]: already_have_one = True break if not already_have_one: self.tabs.append(Timeline(self.conn, self.notice_window, tab[0])) elif tab[0] == "profile": screen_name = tab[1] if screen_name[0] == "@": screen_name = screen_name[1:] self.tabs.append(Profile(self.conn, self.notice_window, screen_name)) elif tab[0] == "user": screen_name = tab[1] if screen_name[0] == "@": screen_name = screen_name[1:] user_id = self.conn.users_show(screen_name=screen_name)['id'] self.tabs.append(Timeline(self.conn, self.notice_window, "user", {'screen_name':screen_name, 'user_id':user_id})) elif tab[0] == "group": nickname = tab[1] if nickname[0] == "!": nickname = nickname[1:] group_id = int(self.conn.statusnet_groups_show(nickname=nickname)['id']) self.tabs.append(Timeline(self.conn, self.notice_window, "group", {'nickname':nickname, 'group_id':group_id})) elif tab[0] == "tag": tag = tab[1] if tag[0] == "#": tag = tag[1:] self.tabs.append(Timeline(self.conn, self.notice_window, "tag", {'tag':tag})) elif tab[0] == "search": self.tabs.append(Timeline(self.conn, self.notice_window, "search", {'query':tab[1]})) #not too sure why anyone would need to auto-open these last two, but it couldn't hurt to add them elif tab[0] == "context": notice_id = int(tab[1]) self.tabs.append(Timeline(self.conn, self.notice_window, "context", {'notice_id':notice_id})) elif tab[0] == "conversation": conv_id = int(tab[1]) self.tabs.append(Timeline(self.conn, self.notice_window, "context", {'conversation_id':conv_id})) elif tab[0] == "help": self.tabs.append(Help(self.notice_window, self.path)) self.update_timer = Timer(config.config['update_interval'], self.update_tabs) self.update_timer.start() self.current_tab = 0 self.tabs[self.current_tab].active = True self.tab_order = range(len(self.tabs)) self.tab_bar = TabBar(self.tab_bar_window) self.tab_bar.tabs = [tab.name for tab in self.tabs] self.tab_bar.current_tab = self.current_tab self.tab_bar.update() self.update_tabs() self.display_current_tab() self.loop() def update_tabs(self): self.update_timer.cancel() if self.insert_mode == False: self.status_bar.update("Updating Timelines...") self.tab_bar.tabs = [tab.name for tab in self.tabs] self.tab_bar.current_tab = self.current_tab self.tab_bar.update() TabUpdater(self.tabs, self, 'end_update_tabs').start() else: self.update_timer = Timer(config.config['update_interval'], self.update_tabs) def end_update_tabs(self): self.display_current_tab() if config.session_store.update_error is not None: self.status_bar.timed_update(config.session_store.update_error) self.status_bar.do_nothing() self.tab_bar.tabs = [tab.name for tab in self.tabs] self.tab_bar.current_tab = self.current_tab self.tab_bar.update() self.update_timer = Timer(config.config['update_interval'], self.update_tabs) self.update_timer.start() def update_tab_buffers(self): for tab in self.tabs: tab.update_buffer() def display_current_tab(self): self.tabs[self.current_tab].display() self.tab_bar.tabs = [tab.name for tab in self.tabs] self.tab_bar.current_tab = self.current_tab self.tab_bar.update() def close_current_tab(self): if len(self.tabs) == 1: pass else: del self.tabs[self.current_tab] del self.tab_order[0] for index in range(len(self.tab_order)): if self.tab_order[index] > self.current_tab: self.tab_order[index] -= 1 self.current_tab = self.tab_order[0] self.tabs[self.current_tab].active = True self.display_current_tab() def loop(self): self.running = True while self.running: input = self.main_window.getch() if self.qreply == False: switch_to_tab = None for x in range(0, len(self.tabs)): if x >= 9: break if input == ord(str(x+1)): switch_to_tab = x if input in self.keybindings['nexttab']: if self.current_tab < (len(self.tabs) - 1): switch_to_tab = self.current_tab + 1 elif input in self.keybindings['nexttabcycle']: if self.current_tab < (len(self.tabs) - 1): switch_to_tab = self.current_tab + 1 else: switch_to_tab = 0 elif input in self.keybindings['prevtab']: if self.current_tab >= 1: switch_to_tab = self.current_tab - 1 elif input in self.keybindings['prevtabcycle']: if self.current_tab >= 1: switch_to_tab = self.current_tab - 1 else: switch_to_tab = len(self.tabs) - 1 elif input in self.keybindings['tabswapright']: if self.current_tab < (len(self.tabs) - 1): self.tabs[self.current_tab], self.tabs[self.current_tab+1] = self.tabs[self.current_tab+1], self.tabs[self.current_tab] switch_to_tab = self.current_tab + 1 elif input in self.keybindings['tabswapleft']: if self.current_tab >= 1: self.tabs[self.current_tab-1], self.tabs[self.current_tab] = self.tabs[self.current_tab], self.tabs[self.current_tab-1] switch_to_tab = self.current_tab - 1 if switch_to_tab is not None: self.tab_order.insert(0, self.tab_order.pop(self.tab_order.index(switch_to_tab))) self.tabs[self.current_tab].active = False self.current_tab = switch_to_tab self.tabs[self.current_tab].active = True else: for x in range(1, 9): if input == ord(str(x)): self.update_timer.cancel() self.insert_mode = True self.parse_input(self.text_entry.edit("/r " + str(x) + " ")) self.qreply = False if input in self.keybindings['scrollup']: self.tabs[self.current_tab].scrollup(1) self.display_current_tab() elif input in self.keybindings['scrolltop']: self.tabs[self.current_tab].scrollup(0) self.display_current_tab() elif input in self.keybindings['pageup']: self.tabs[self.current_tab].scrollup(self.main_window.getmaxyx()[0] - 11) # the 11 offset gives 2 lines of overlap between the pre-scroll view and post-scroll view self.display_current_tab() elif input in self.keybindings['scrolldown']: self.tabs[self.current_tab].scrolldown(1) self.display_current_tab() elif input in self.keybindings['scrollbottom']: self.tabs[self.current_tab].scrolldown(0) self.display_current_tab() elif input in self.keybindings['pagedown']: self.tabs[self.current_tab].scrolldown(self.main_window.getmaxyx()[0] - 11) # as above self.display_current_tab() elif input in self.keybindings['firstpage']: if self.tabs[self.current_tab].prevpage(0): self.status_bar.update("Moving to first page...") self.tabs[self.current_tab].update() self.status_bar.do_nothing() elif input in self.keybindings['newerpage']: if self.tabs[self.current_tab].prevpage(): self.status_bar.update("Moving to newer page...") self.tabs[self.current_tab].update() self.status_bar.do_nothing() elif input in self.keybindings['olderpage']: if self.tabs[self.current_tab].nextpage(): self.status_bar.update("Moving to older page...") self.tabs[self.current_tab].update() self.status_bar.do_nothing() elif input in self.keybindings['refresh']: self.update_tabs() elif input in self.keybindings['input']: self.update_timer.cancel() self.insert_mode = True self.parse_input(self.text_entry.edit()) elif input in self.keybindings['commandinput']: self.update_timer.cancel() self.insert_mode = True self.parse_input(self.text_entry.edit("/")) elif input in self.keybindings['search']: self.update_timer.cancel() self.insert_mode = True self.search_mode = True self.parse_search(self.text_entry.edit()) elif input in self.keybindings['quit']: self.running = False elif input in self.keybindings['closetab']: self.close_current_tab() elif input in self.keybindings['help']: self.tabs.append(Help(self.notice_window, self.path)) self.tabs[self.current_tab].active = False self.current_tab = len(self.tabs) - 1 self.tabs[self.current_tab].active = True self.tab_order.insert(0, self.current_tab) self.tabs[self.current_tab].update() elif input in self.keybindings['qreply']: self.qreply = True elif input in self.keybindings['nextmatch']: if (self.last_page_search['query'] != "") and (self.last_page_search['tab'] == self.current_tab): if self.last_page_search['viewing'] < (len(self.last_page_search['occurs']) - 1): self.last_page_search['viewing'] += 1 else: self.last_page_search['viewing'] = 0 self.tabs[self.current_tab].scrollto(self.last_page_search['occurs'][self.last_page_search['viewing']]) self.tabs[self.current_tab].search_highlight_line = self.last_page_search['occurs'][self.last_page_search['viewing']] if self.last_page_search['viewing'] == 0: self.status_bar.update("Viewing result #%d for '%s' (search hit BOTTOM, continuing at TOP)" % (self.last_page_search['viewing'] + 1, self.last_page_search['query'])) else: self.status_bar.update("Viewing result #%d for '%s'" % (self.last_page_search['viewing'] + 1, self.last_page_search['query'])) self.display_current_tab() elif input in self.keybindings['prevmatch']: if (self.last_page_search['query'] != "") and (self.last_page_search['tab'] == self.current_tab): if self.last_page_search['viewing'] > 0: self.last_page_search['viewing'] -= 1 else: self.last_page_search['viewing'] = len(self.last_page_search['occurs']) - 1 self.tabs[self.current_tab].scrollto(self.last_page_search['occurs'][self.last_page_search['viewing']]) self.tabs[self.current_tab].search_highlight_line = self.last_page_search['occurs'][self.last_page_search['viewing']] if self.last_page_search['viewing'] == (len(self.last_page_search['occurs']) - 1): self.status_bar.update("Viewing result #%d for '%s' (search hit TOP, continuing at BOTTOM)" % (self.last_page_search['viewing'] + 1, self.last_page_search['query'])) else: self.status_bar.update("Viewing result #%d for '%s'" % (self.last_page_search['viewing'] + 1, self.last_page_search['query'])) self.display_current_tab() elif input == curses.ascii.ctrl(ord("l")): self.redraw() elif input in self.keybindings['pausetoggleall']: for tab in self.tabs: if hasattr(tab, "timeline"): tab.paused = not tab.paused if tab.paused and (len(tab.timeline) > 0): self.tabs[self.current_tab].timeline[0]["ic__paused_on"] = True tab.update_buffer() tab.update_name() self.tab_bar.tabs = [tab.name for tab in self.tabs] self.tab_bar.current_tab = self.current_tab self.tab_bar.update() elif input in self.keybindings['togglenoticelinks']: config.config["show_notice_links"] = not config.config["show_notice_links"] self.update_tab_buffers() # and now the c* actions, and anything else that shouldn't run on non-timeline tabs if isinstance(self.tabs[self.current_tab], Timeline) and len(self.tabs[self.current_tab].timeline) > 0: # don't try to do the c* actions unless on a populated timeline if (self.tabs[self.current_tab].chosen_one + 1) > len(self.tabs[self.current_tab].timeline): # reduce chosen_one if it's beyond the end self.tabs[self.current_tab].chosen_one = len(self.tabs[self.current_tab].timeline) - 1 self.tabs[self.current_tab].update_buffer() if input in self.keybindings['creply']: self.update_timer.cancel() self.insert_mode = True if "direct" in self.tabs[self.current_tab].timeline_type: self.parse_input(self.text_entry.edit("/dm " + str(self.tabs[self.current_tab].chosen_one + 1) + " ")) else: self.parse_input(self.text_entry.edit("/r " + str(self.tabs[self.current_tab].chosen_one + 1) + " ")) elif input in self.keybindings['creplymode']: self.update_timer.cancel() if "direct" in self.tabs[self.current_tab].timeline_type: self.insert_mode = True self.parse_input(self.text_entry.edit("/dm " + str(self.tabs[self.current_tab].chosen_one + 1) + " ")) else: try: self.cmd_reply(self.tabs[self.current_tab].timeline[self.tabs[self.current_tab].chosen_one]) except Exception, (errmsg): self.status_bar.timed_update("ERROR: Couldn't post status: %s" % (errmsg)) elif input in self.keybindings['cdelete']: try: self.cmd_delete(self.tabs[self.current_tab].timeline[self.tabs[self.current_tab].chosen_one]) except Exception, (errmsg): self.status_bar.timed_update("ERROR: Couldn't delete notice: %s" % (errmsg)) elif input in self.keybindings['cnext']: if self.tabs[self.current_tab].chosen_one != (len(self.tabs[self.current_tab].timeline) - 1): self.tabs[self.current_tab].chosen_one += 1 self.tabs[self.current_tab].update_buffer() self.tabs[self.current_tab].scrolltodent(self.tabs[self.current_tab].chosen_one, smooth_scroll=config.config["smooth_cscroll"]) elif input in self.keybindings['cprev']: if self.tabs[self.current_tab].chosen_one != 0: self.tabs[self.current_tab].chosen_one -= 1 self.tabs[self.current_tab].update_buffer() self.tabs[self.current_tab].scrolltodent(self.tabs[self.current_tab].chosen_one, smooth_scroll=config.config["smooth_cscroll"]) elif input in self.keybindings['cfirst']: if self.tabs[self.current_tab].chosen_one != 0: self.tabs[self.current_tab].chosen_one = 0 self.tabs[self.current_tab].update_buffer() self.tabs[self.current_tab].scrolltodent(self.tabs[self.current_tab].chosen_one) elif input in self.keybindings['clast']: last_index = len(self.tabs[self.current_tab].timeline) - 1 if self.tabs[self.current_tab].chosen_one != last_index: self.tabs[self.current_tab].chosen_one = last_index self.tabs[self.current_tab].update_buffer() self.tabs[self.current_tab].scrolltodent(self.tabs[self.current_tab].chosen_one) elif input in self.keybindings['cfav']: try: self.cmd_favourite(self.tabs[self.current_tab].timeline[self.tabs[self.current_tab].chosen_one]) except Exception, (errmsg): self.status_bar.timed_update("ERROR: Couldn't favourite notice: %s" % (errmsg)) elif input in self.keybindings['cunfav']: try: self.cmd_unfavourite(self.tabs[self.current_tab].timeline[self.tabs[self.current_tab].chosen_one]) except Exception, (errmsg): self.status_bar.timed_update("ERROR: Couldn't unfavourite notice: %s" % (errmsg)) elif input in self.keybindings['crepeat']: can_repeat = True try: if self.tabs[self.current_tab].timeline_type in ["direct", "sentdirect"]: can_repeat = False except AttributeError: pass # we must be in a Context tab, so repeating is fine. if can_repeat: try: self.cmd_repeat(self.tabs[self.current_tab].timeline[self.tabs[self.current_tab].chosen_one]) except Exception, (errmsg): self.status_bar.timed_update("ERROR: Couldn't repeat notice: %s" % (errmsg)) elif input in self.keybindings['cquote']: can_repeat = True try: if self.tabs[self.current_tab].timeline_type in ["direct", "sentdirect"]: can_repeat = False except AttributeError: pass # we must be in a Context tab, so repeating is fine. if can_repeat: self.update_timer.cancel() self.cmd_quote(self.tabs[self.current_tab].timeline[self.tabs[self.current_tab].chosen_one]) elif input in self.keybindings['ccontext']: try: self.cmd_context(self.tabs[self.current_tab].timeline[self.tabs[self.current_tab].chosen_one]) except Exception, (errmsg): self.status_bar.timed_update("ERROR: Couldn't get context: %s" % (errmsg)) elif input in self.keybindings['pausetoggle']: self.tabs[self.current_tab].paused = not self.tabs[self.current_tab].paused if self.tabs[self.current_tab].paused and (len(self.tabs[self.current_tab].timeline) > 0): self.tabs[self.current_tab].timeline[0]["ic__paused_on"] = True self.tabs[self.current_tab].update_buffer() # get the pauseline drawn self.tabs[self.current_tab].update_name() # force the tab names to update self.tab_bar.tabs = [tab.name for tab in self.tabs] self.tab_bar.current_tab = self.current_tab self.tab_bar.update() elif input in self.keybindings['mute']: try: self.cmd_mute(self.tabs[self.current_tab].timeline[self.tabs[self.current_tab].chosen_one]) except Exception, (errmsg): self.status_bar.timed_update("ERROR: Couldn't mute: %s" % (errmsg)) elif input in self.keybindings['unmute']: try: self.cmd_unmute(self.tabs[self.current_tab].timeline[self.tabs[self.current_tab].chosen_one]) except Exception, (errmsg): self.status_bar.timed_update("ERROR: Couldn't unmute: %s" % (errmsg)) y, x = self.screen.getmaxyx() if y != self.y or x != self.x: self.redraw() self.update_tab_buffers() self.display_current_tab() self.status_window.refresh() self.main_window.refresh() self.quit(); def validate(self, param): if type(param) == type([]): guess_list = param self.status_bar.timed_update(" ".join(guess_list), 2) else: character_count = param if self.quote_mode: if self.conn.length_limit == 0: self.status_bar.update("Quote Mode: " + str(character_count)) else: self.status_bar.update("Quote Mode: " + str(self.conn.length_limit - character_count)) elif self.reply_mode: if self.conn.length_limit == 0: self.status_bar.update("Reply Mode: " + str(character_count)) else: self.status_bar.update("Reply Mode: " + str(self.conn.length_limit - character_count)) elif self.search_mode: if self.last_page_search['query'] != "": self.status_bar.update("In-page Search (last search: '%s')" % (self.last_page_search['query'])) else: self.status_bar.update("In-page Search") else: if self.conn.length_limit == 0: self.status_bar.update("Insert Mode: " + str(character_count)) else: self.status_bar.update("Insert Mode: " + str(self.conn.length_limit - character_count)) def parse_input(self, input): update = False new_tab = False if input is None: input = "" if len(input) > 0: # don't do anything if the user didn't enter anything input = input.rstrip() tokens = [token for token in input.split(" ") if token != ""] if tokens[0][0] == "i" and ((tokens[0][1:] in self.known_commands) or (tokens[0][1:] in config.config["aliases"])): tokens[0] = tokens[0][1:] # avoid doing the wrong thing when people accidentally submit stuff like "i/r 2 blabla" for command in self.known_commands: # catch mistakes like "/r1" - the last condition is so that, for example, "/directs" is not mistakenly converted to "/direct s" if (tokens[0][:len(command)] == command) and (tokens[0] != command) and not (tokens[0] in self.known_commands) and not (tokens[0] in config.config['aliases']): tokens[:1] = [command, tokens[0].replace(command, "")] # catch mis-capitalizations if tokens[0].lower() == command.lower() and not tokens[0].lower() in [cmd.lower() for cmd in self.known_commands if cmd != command]: tokens[0] = command for alias in config.config['aliases']: # catch mistakes like "/r1" - the last condition is so that, for example, "/directs" is not mistakenly converted to "/direct s" if (tokens[0][:len(alias)] == alias) and (tokens[0] != alias) and not (tokens[0] in self.known_commands) and not (tokens[0] in config.config['aliases']): tokens[:1] = [alias, tokens[0].replace(alias, "")] # catch mis-capitalizations if tokens[0].lower() == alias.lower() and not tokens[0].lower() in [als.lower() for als in config.config['aliases'] if als != alias]: tokens[0] = alias if tokens[0] in config.config["aliases"]: tokens = config.config["aliases"][tokens[0]].split(" ") + tokens[1:] try: if ("direct" in self.tabs[self.current_tab].timeline_type) and (tokens[0] == "/reply"): tokens[0] = "/direct" except AttributeError: # the tab has no timeline_type, so it's *definitely* not directs. pass if tokens[0] in self.known_commands: try: if tokens[0] == "/reply" and len(tokens) >= 2: self.status_bar.update("Posting Reply...") try: try: float(tokens[1]) except ValueError: user = tokens[1] if user[0] == "@": user = user[1:] update = self.cmd_mention(user, " ".join(tokens[2:])) else: update = self.cmd_reply(self.tabs[self.current_tab].timeline[int(tokens[1]) - 1], " ".join(tokens[2:])) except Exception, (errmsg): self.status_bar.timed_update("ERROR: Couldn't post status: %s" % (errmsg)) elif tokens[0] == "/favourite" and len(tokens) == 2: self.cmd_favourite(self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]) elif tokens[0] == "/unfavourite" and len(tokens) == 2: self.cmd_unfavourite(self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]) elif tokens[0] == "/repeat" and len(tokens) == 2: update = self.cmd_repeat(self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]) elif tokens[0] == "/direct" and len(tokens) >= 3: try: float(tokens[1]) except ValueError: screen_name = tokens[1] if screen_name[0] == "@": screen_name = screen_name[1:] else: if "direct" in self.tabs[self.current_tab].timeline_type: screen_name = self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]['sender']['screen_name'] else: if "retweeted_status" in self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]: screen_name = self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]['retweeted_status']['user']['screen_name'] else: screen_name = self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]['user']['screen_name'] self.cmd_direct(screen_name, " ".join(tokens[2:])) elif tokens[0] == "/delete" and len(tokens) == 2: self.cmd_delete(self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]) elif tokens[0] == "/profile" and len(tokens) == 2: # Yeuch try: float(tokens[1]) except ValueError: user = tokens[1] if user[0] == "@": user = user[1:] else: if "retweeted_status" in self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]: user = self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]["retweeted_status"]["user"]["screen_name"] else: user = self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]["user"]["screen_name"] self.cmd_profile(user) elif tokens[0] == "/spamreport" and len(tokens) >= 3: # Yeuch try: float(tokens[1]) except ValueError: username = tokens[1] if username[0] == "@": username = username[1:] else: if "retweeted_status" in self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]: username = self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]["retweeted_status"]["user"]["screen_name"] else: username = self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]["user"]["screen_name"] update = self.cmd_spamreport(username, " ".join(tokens[2:])) elif tokens[0] == "/block" and len(tokens) >= 2: for token in tokens[1:]: # Yeuch try: float(token) except ValueError: user = token if user[0] == "@": user = user[1:] else: if "retweeted_status" in self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]: user = self.tabs[self.current_tab].timeline[int(token) - 1]["retweeted_status"]["user"]["screen_name"] else: user = self.tabs[self.current_tab].timeline[int(token) - 1]["user"]["screen_name"] self.cmd_block(user) elif tokens[0] == "/unblock" and len(tokens) >= 2: self.status_bar.update("Removing Block(s)...") for token in tokens[1:]: # Yeuch try: float(token) except ValueError: user = token if user[0] == "@": user = user[1:] else: if "retweeted_status" in self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]: user = self.tabs[self.current_tab].timeline[int(token) - 1]["retweeted_status"]["user"]["screen_name"] else: user = self.tabs[self.current_tab].timeline[int(token) - 1]["user"]["screen_name"] self.cmd_unblock(user) elif tokens[0] == "/user" and len(tokens) == 2: # Yeuch try: float(tokens[1]) except ValueError: user = tokens[1] if user[0] == "@": user = user[1:] else: if "retweeted_status" in self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]: user = self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]["retweeted_status"]["user"]["screen_name"] else: user = self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]["user"]["screen_name"] self.cmd_user(user) elif tokens[0] == "/context" and len(tokens) == 2: self.cmd_context(self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]) elif tokens[0] == "/subscribe" and len(tokens) == 2: # Yeuch try: float(tokens[1]) except ValueError: user = tokens[1] if user[0] == "@": user = user[1:] else: if "retweeted_status" in self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]: user = self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]["retweeted_status"]["user"]["screen_name"] else: user = self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]["user"]["screen_name"] self.cmd_subscribe(user) elif tokens[0] == "/unsubscribe" and len(tokens) == 2: # Yeuch try: float(tokens[1]) except ValueError: user = tokens[1] if user[0] == "@": user = user[1:] else: if "retweeted_status" in self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]: user = self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]["retweeted_status"]["user"]["screen_name"] else: user = self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]["user"]["screen_name"] self.cmd_unsubscribe(user) elif tokens[0] == "/group" and len(tokens) == 2: self.status_bar.update("Loading Group Timeline...") group = tokens[1] if group[0] == "!": group = group[1:] self.cmd_group(group) elif tokens[0] == "/groupjoin" and len(tokens) == 2: self.status_bar.update("Joining Group...") group = tokens[1] if group[0] == "!": group = group[1:] self.cmd_groupjoin(group) elif tokens[0] == "/groupleave" and len(tokens) == 2: self.status_bar.update("Leaving Group...") group = tokens[1] if group[0] == "!": group = group[1:] self.cmd_groupleave(group) elif tokens[0] == "/groupmember" and len(tokens) == 2: self.status_bar.update("Checking membership...") group = tokens[1] if group[0] == "!": group = group[1:] self.cmd_groupmember(group) elif tokens[0] == "/tag" and len(tokens) == 2: self.status_bar.update("Loading Tag Timeline...") tag = tokens[1] if tag[0] == "#": tag = tag[1:] self.cmd_tag(tag) elif tokens[0] == "/sentdirects" and len(tokens) == 1: self.cmd_sentdirects() elif tokens[0] == "/favourites" and len(tokens) == 1: self.cmd_favourites() elif tokens[0] == "/search" and len(tokens) >= 2: query = " ".join(tokens[1:]) self.cmd_search(query) elif tokens[0] == "/home" and len(tokens) == 1: self.cmd_home() elif tokens[0] == "/mentions" and len(tokens) == 1: self.cmd_mentions() elif tokens[0] == "/directs" and len(tokens) == 1: self.cmd_directs() elif tokens[0] == "/public" and len(tokens) == 1: self.cmd_public() elif tokens[0] == "/config" and len(tokens) >= 3: self.cmd_config(tokens[1], " ".join(tokens[2:])) elif tokens[0] == "/alias" and len(tokens) >= 3: self.cmd_alias(tokens[1], " ".join(tokens[2:])) elif tokens[0] == "/link" and len(tokens) >= 2: if len(tokens) == 2: # only notice number given, assume first link self.cmd_link(self.tabs[self.current_tab].timeline[int(tokens[1]) - 1], 1) else: # notice number and link number given self.cmd_link(self.tabs[self.current_tab].timeline[int(tokens[2]) - 1], tokens[1]) elif tokens[0] == "/bugreport" and len(tokens) >= 2: update = self.cmd_bugreport(" ".join(tokens[1:])) elif tokens[0] == "/featurerequest" and len(tokens) >= 2: update = self.cmd_featurerequest(" ".join(tokens[1:])) elif tokens[0] == "/quote" and len(tokens) == 2: update = self.cmd_quote(self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]) elif tokens[0] == "/mute" and len(tokens) == 2: update = self.cmd_mute(self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]) elif tokens[0] == "/unmute" and len(tokens) == 2: update = self.cmd_unmute(self.tabs[self.current_tab].timeline[int(tokens[1]) - 1]) elif tokens[0] == "/quit" and len(tokens) == 1: self.running = False except StatusNetError, e: self.status_bar.timed_update("Status.Net error %d: %s" % (e.errcode, e.details)) else: try: update = self.cmd_post(input) except StatusNetError, e: self.status_bar.timed_update("Status.Net error %d: %s" % (e.errcode, e.details)) if not update: self.tabs[self.current_tab].update() self.status_bar.do_nothing() self.text_entry = Textbox(self.entry_window, self.validate, insert_mode=True) self.text_entry.stripspaces = 1 self.tabs[self.current_tab].search_highlight_line = -1 self.display_current_tab() self.status_bar.do_nothing() self.insert_mode = False self.update_timer = Timer(config.config['update_interval'], self.update_tabs) self.update_timer.start() def opens_tab(fail_on_exists=None): # decorator factory, creates decorators to deal with the standard switching to tab stuff def tabopen_decorator(cmd): def outcmd(*largs, **kargs): self = largs[0] already_have_one = False if fail_on_exists is not None: for tab in self.tabs: if hasattr(tab, "timeline_type") and tab.timeline_type == fail_on_exists: already_have_one = True break if not already_have_one: self.tabs.append(cmd(*largs, **kargs)) self.tabs[self.current_tab].active = False self.current_tab = len(self.tabs) - 1 self.tabs[self.current_tab].active = True self.tab_order.insert(0, self.current_tab) self.tabs[self.current_tab].update() return True return outcmd return tabopen_decorator def shows_status(status_msg): # decorator factory, creates status decorators for commands that show statuses def status_decorator(cmd): def outcmd(*largs, **kargs): self = largs[0] self.status_bar.update(status_msg + "...") retval = cmd(*largs, **kargs) self.status_bar.do_nothing() return retval return outcmd return status_decorator def posts_notice(cmd): # decorator which inserts the newly-posted notice(s) for commands that post def outcmd(*largs, **kargs): self = largs[0] update = cmd(*largs, **kargs) if update is not None: update["ic__raw_datetime"] = helpers.normalise_datetime(update["created_at"]) update["ic__from_web"] = False if self.tabs[self.current_tab].name == "Context": # if we're in a context tab, add notice to there too self.tabs[self.current_tab].timeline.insert(0, update) self.tabs[self.current_tab].update_buffer() for tab in self.tabs: if not hasattr(tab, 'timeline_type'): continue if tab.timeline_type == "home": if isinstance(update, list): for notice in update: tab.timeline.insert(0, notice) else: tab.timeline.insert(0, update) tab.update_buffer() self.status_bar.do_nothing() return True return outcmd def repeat_passthrough(cmd): # decorator which unpacks repeats for any commands that should handle only the original notice def outcmd(*largs, **kargs): # requires that notice is the *second* argument, for now at least largs = list(largs) # largs is a tuple, we need it mutable if "retweeted_status" in largs[1]: largs[1] = largs[1]["retweeted_status"] return cmd(*largs, **kargs) return outcmd @shows_status("Posting reply") @posts_notice @repeat_passthrough def cmd_reply(self, notice, message=""): user = notice["user"]["screen_name"] if message == "": self.reply_mode = True if config.config["new_reply_mode"]: status = self.text_entry.edit("") else: status = self.text_entry.edit("@%s " % (user)) self.reply_mode = False else: if config.config["new_reply_mode"]: status = message else: status = "@%s %s" % (notice["user"]["screen_name"], message) if status is None: status = "" if len(status) > 0: if config.config["new_reply_mode"]: dup_first_word = False else: dup_first_word = True return self.conn.statuses_update(status, "IdentiCurse", int(notice["id"]), long_dent=config.config["long_dent"], dup_first_word=dup_first_word) @shows_status("Posting mention") @posts_notice def cmd_mention(self, username, message): status = "@%s %s" % (username, message) return self.conn.statuses_update(status, "IdentiCurse", long_dent=config.config["long_dent"], dup_first_word=True) @shows_status("Posting notice") @posts_notice def cmd_post(self, message): if message is None: message = "" if len(message) > 0: return self.conn.statuses_update(message, "IdentiCurse", long_dent=config.config["long_dent"], dup_first_word=False) @shows_status("Favouriting notice") @repeat_passthrough def cmd_favourite(self, notice): self.conn.favorites_create(notice["id"]) @shows_status("Unfavouriting notice") @repeat_passthrough def cmd_unfavourite(self, notice): self.conn.favorites_destroy(notice["id"]) @shows_status("Repeating notice") @posts_notice @repeat_passthrough def cmd_repeat(self, notice): return self.conn.statuses_retweet(notice["id"], source="IdentiCurse") @shows_status("Quoting notice") @posts_notice @repeat_passthrough def cmd_quote(self, notice): self.quote_mode = True new_status_base_unclean = "RD @%s %s" % (notice['user']['screen_name'], notice['text']) new_status_base_clean = "" for entity in helpers.split_entities(new_status_base_unclean): if entity['type'] == "group": entity['text'] = "#" + entity['text'][1:] new_status_base_clean += entity['text'] status = self.text_entry.edit(new_status_base_clean) self.quote_mode = False if status is None: status = "" if len(status) > 0: return self.conn.statuses_update(status, "IdentiCurse", int(notice["id"]), long_dent=config.config["long_dent"], dup_first_word=False) @shows_status("Sending direct message") def cmd_direct(self, username, message): user_id = self.conn.users_show(screen_name=username)['id'] self.conn.direct_messages_new(username, user_id, message, source="IdentiCurse") @shows_status("Deleting notice") def cmd_delete(self, notice): delete_succeeded = False try: self.conn.statuses_destroy(notice["id"]) delete_succeeded = True except StatusNetError, e: if e.errcode == 403: if "retweeted_status" in notice: # user doesn't own the repeat, so is probably trying to delete the original status self.conn.statuses_destroy(notice["retweeted_status"]["id"]) delete_succeeded = True else: # user is trying to delete something they don't own. the API doesn't like this self.status_bar.timed_update("You cannot delete others' notices.", 3) else: # it wasn't a 403, so re-raise raise(e) try: self.conn.statuses_destroy(notice["id"]) # for now, we try it twice, since identi.ca at least seems to have an issue where deleting must be done twice except: pass # since we should've already got it (in an ideal situation), ignore the errors from this attempt. if delete_succeeded: for tab in [tab for tab in self.tabs if hasattr(tab, "timeline_type")]: n_id = notice["id"] # keep this in a variable of it's own, so deleting the original notice doesn't break the test in the next bit for tl_notice in tab.timeline: if tl_notice["id"] == n_id: tab.timeline.remove(tl_notice) tab.update_buffer() @shows_status("Loading profile") @opens_tab() def cmd_profile(self, username): return Profile(self.conn, self.notice_window, username) @shows_status("Deploying orbital nukes") @posts_notice def cmd_spamreport(self, username, reason=""): target_user_id = self.conn.users_show(screen_name=username)["id"] status = "@support !sr %s UID %d" % (username, target_user_id) if len(reason) > 0: status += " %s" % (reason) user_id = self.conn.users_show()['id'] group_id = self.conn.statusnet_groups_show(nickname="spamreport")['id'] if not self.conn.statusnet_groups_is_member(user_id, group_id): self.status_bar.timed_update("You are not a member of the !spamreport group. Joining it is highly recommended if reporting spam.") self.conn.blocks_create(user_id=target_user_id, screen_name=username) return self.conn.statuses_update(status, "IdentiCurse") @shows_status("Blocking user") def cmd_block(self, username): user_id = self.conn.users_show(screen_name=username)["id"] self.conn.blocks_create(user_id=user_id, screen_name=username) @shows_status("Unblocking user") def cmd_unblock(self, username): user_id = self.conn.users_show(screen_name=username)["id"] self.conn.blocks_destroy(user_id=user_id, screen_name=username) @shows_status("Loading user timeline") @opens_tab() def cmd_user(self, username): user_id = self.conn.users_show(screen_name=username)["id"] return Timeline(self.conn, self.notice_window, "user", {'user_id':user_id, 'screen_name':username}) @shows_status("Loading group timeline") @opens_tab() def cmd_group(self, group): group_id = int(self.conn.statusnet_groups_show(nickname=group)['id']) return Timeline(self.conn, self.notice_window, "group", {'group_id':group_id, 'nickname':group}) @shows_status("Loading tag timeline") @opens_tab() def cmd_tag(self, tag): return Timeline(self.conn, self.notice_window, "tag", {'tag':tag}) @shows_status("Loading context") @opens_tab() @repeat_passthrough def cmd_context(self, notice): if "statusnet_conversation_id" in notice: return Timeline(self.conn, self.notice_window, "context", {'conversation_id':notice['statusnet_conversation_id']}) else: return Timeline(self.conn, self.notice_window, "context", {'notice_id':notice['id']}) @shows_status("Subscribing to user") def cmd_subscribe(self, username): user_id = self.conn.users_show(screen_name=username)["id"] self.conn.friendships_create(user_id=user_id, screen_name=username) @shows_status("Unsubscribing from user") def cmd_unsubscribe(self, username): user_id = self.conn.users_show(screen_name=username)["id"] self.conn.friendships_destroy(user_id=user_id, screen_name=username) @shows_status("Joining group") def cmd_groupjoin(self, group): group_id = int(self.conn.statusnet_groups_show(nickname=group)['id']) self.conn.statusnet_groups_join(group_id=group_id, nickname=group) @shows_status("Leaving group") def cmd_groupleave(self, group): group_id = int(self.conn.statusnet_groups_show(nickname=group)['id']) self.conn.statusnet_groups_leave(group_id=group_id, nickname=group) @shows_status("Checking if you are a member of that group") def cmd_groupmember(self, group): user_id = self.conn.users_show()['id'] group_id = self.conn.statusnet_groups_show(nickname=group)['id'] if self.conn.statusnet_groups_is_member(user_id, group_id): self.status_bar.timed_update("You are a member of !%s." % (group)) else: self.status_bar.timed_update("You are not a member of !%s." % (group)) @shows_status("Loading received direct messages") @opens_tab("direct") def cmd_directs(self): return Timeline(self.conn, self.notice_window, "direct") @shows_status("Loading sent direct messages") @opens_tab("sentdirect") def cmd_sentdirects(self): return Timeline(self.conn, self.notice_window, "sentdirect") @shows_status("Loading favourites") @opens_tab("favourites") def cmd_favourites(self): return Timeline(self.conn, self.notice_window, "favourites") @shows_status("Searching") @opens_tab() def cmd_search(self, query): return Timeline(self.conn, self.notice_window, "search", type_params={'query':query}) @shows_status("Loading home timeline") @opens_tab("home") def cmd_home(self): return Timeline(self.conn, self.notice_window, "home") @shows_status("Loading mentions timeline") @opens_tab("mentions") def cmd_mentions(self): return Timeline(self.conn, self.notice_window, "mentions") @shows_status("Loading public timeline") @opens_tab("public") def cmd_public(self): return Timeline(self.conn, self.notice_window, "public") @shows_status("Changing config") def cmd_config(self, key, value): key = key.split('.') if len(key) == 2: # there has to be a clean way to avoid hardcoded len checks, but I can't think what right now, and technically it works for all currently valid config keys config.config[key[0]][key[1]] = value else: config.config[key[0]] = value config.config.save() @shows_status("Aliasing command") def cmd_alias(self, alias, command): if alias[0] != "/": alias = "/" + alias if command[0] != "/": command = "/" + command config.config["aliases"][alias] = command config.config.save() @shows_status("Opening link(s)") @repeat_passthrough def cmd_link(self, notice, link_num): links_to_open = [] target_urls = helpers.url_regex.findall(notice["text"]) if link_num == "*": if not target_urls: self.status_bar.timed_update("No matching link(s) found.") return for target_url in target_urls: if not target_url in links_to_open: links_to_open.append(target_url) else: link_index = int(link_num) - 1 try: target_url = target_urls[link_index] if not target_url in links_to_open: links_to_open.append(target_url) except IndexError: self.status_bar.timed_update("No matching link(s) found.") for link in links_to_open: subprocess.Popen(config.config['browser'] % (link), shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE) @shows_status("Sending bug report") @posts_notice def cmd_bugreport(self, report): status = "#icursebug " + report return self.conn.statuses_update(status, "IdentiCurse", long_dent=config.config['long_dent'], dup_first_word=True) @shows_status("Sending feature request") @posts_notice def cmd_featurerequest(self, request): status = "#icurserequest " + request return self.conn.statuses_update(status, "IdentiCurse", long_dent=config.config['long_dent'], dup_first_word=True) @shows_status("Muting conversation") @repeat_passthrough def cmd_mute(self, notice): if (not "statusnet_conversation_id" in notice) or (notice["statusnet_conversation_id"] is None): self.status_bar.timed_update("This instance does not provide conversation IDs, so muting will not be possible.") return if not hasattr(config.session_store, "muted_conversations"): config.session_store.muted_conversations = [] if notice["statusnet_conversation_id"] in config.session_store.muted_conversations: self.status_bar.timed_update("This conversation is already muted.") else: config.session_store.muted_conversations.append(notice["statusnet_conversation_id"]) @shows_status("Unmuting conversation") @repeat_passthrough def cmd_unmute(self, notice): if (not "statusnet_conversation_id" in notice) or (notice["statusnet_conversation_id"] is None): self.status_bar.timed_update("This instance does not provide conversation IDs, so unmuting will not be possible.") return if not hasattr(config.session_store, "muted_conversations"): config.session_store.muted_conversations = [] if notice["statusnet_conversation_id"] in config.session_store.muted_conversations: config.session_store.muted_conversations.remove(notice["statusnet_conversation_id"]) else: self.status_bar.timed_update("This conversation wasn't muted.") @shows_status("Quitting") def cmd_quit(self): self.running = False def parse_search(self, query): if query is not None: query = query.rstrip() if query == "": query = self.last_page_search['query'] if (self.last_page_search['query'] == query) and not (query == "") and (self.last_page_search['tab'] == self.current_tab): # this is a continued search if self.last_page_search['viewing'] < (len(self.last_page_search['occurs']) - 1): self.last_page_search['viewing'] += 1 self.tabs[self.current_tab].scrollto(self.last_page_search['occurs'][self.last_page_search['viewing']]) self.tabs[self.current_tab].search_highlight_line = self.last_page_search['occurs'][self.last_page_search['viewing']] self.status_bar.update("Viewing result #%d for '%s'" % (self.last_page_search['viewing'] + 1, query)) self.display_current_tab() else: self.tabs[self.current_tab].search_highlight_line = -1 self.status_bar.update("No more results for '%s'" % (query)) else: # new search maxx = self.tabs[self.current_tab].window.getmaxyx()[1] search_buffer = self.tabs[self.current_tab].buffer.reflowed(maxx - 2) page_search = {'query':query, 'occurs':[], 'viewing':0, 'tab':self.current_tab} for line_index in range(len(search_buffer)): match_found = False for block in search_buffer[line_index]: if config.config['search_case_sensitive'] == "sensitive": if query in block[0]: match_found = True break else: if query.upper() in block[0].upper(): match_found = True break if match_found: page_search['occurs'].append(line_index) if len(page_search['occurs']) > 0: self.tabs[self.current_tab].scrollto(page_search['occurs'][0]) self.tabs[self.current_tab].search_highlight_line = page_search['occurs'][0] self.status_bar.update("Viewing result #1 for '%s'" % (query)) self.last_page_search = page_search # keep this search else: self.tabs[self.current_tab].search_highlight_line = -1 self.status_bar.update("No results for '%s'" % (query)) self.last_page_search = {'query':"", 'occurs':[], 'viewing':0, 'tab':-1} # reset to no search else: self.status_bar.do_nothing() self.text_entry = Textbox(self.entry_window, self.validate, insert_mode=True) self.text_entry.stripspaces = 1 self.display_current_tab() self.insert_mode = False self.search_mode = False self.update_timer = Timer(config.config['update_interval'], self.update_tabs) self.update_timer.start() def quit(self): try: self.update_timer.cancel() except ValueError: # it may already have been cancelled if it fired shortly before we quit, in which case we can't end it pass curses.endwin() sys.exit() identicurse-0.9+dfsg0/src/identicurse/config.py0000644000175000017500000000711011720536557020631 0ustar mvdanmvdan# -*- coding: utf-8 -*- # # Copyright (C) 2010-2012 Reality and Psychedelic Squid # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os.path try: import json except ImportError: import simplejson as json auth_fields = ["username", "password", "api_path", "consumer_key", "consumer_secret", "oauth_token", "oauth_token_secret", "use_oauth"] class Config(dict): def __init__(self): pass def save(self, filename=None, auth_filename=None): if filename is None: filename = self.filename if auth_filename is None: auth_filename = self.auth_filename try: unclean_config = self.copy() clean_config = {} auth_config = {} for key, value in unclean_config.items(): if key in auth_fields: auth_config[key] = value elif key == "colours" and self.colourscheme_filename is not None: open(self.colourscheme_filename, "w").write(json.dumps(value, indent=4)) else: clean_config[key] = value open(filename, "w").write(json.dumps(clean_config, indent=4)) open(auth_filename, "w").write(json.dumps(auth_config, indent=4)) except IOError: return False return True def load(self, filename=None, auth_filename=None): if filename is None: filename = self.filename if auth_filename is None: auth_filename = self.auth_filename try: self.clear() self.update(json.loads(open(filename, "r").read())) self.update(json.loads(open(auth_filename, "r").read())) if "colourscheme" in self: colours = None try: self.colourscheme_filename = os.path.join(self.basedir, "colours", "%s.json" % (self['colourscheme'])) colourscheme = json.loads(open(self.colourscheme_filename, "r").read()) if not "colours" in colourscheme: colourscheme["colours"] = {} if not "custom_colours" in colourscheme: colourscheme["custom_colours"] = {} colours = {"colours":colourscheme["colours"], "custom_colours":colourscheme["custom_colours"]} except IOError: # couldn't load colourscheme print "Couldn't load your colourscheme (%s) successfully." % (self['colourscheme']) self.colourscheme_filename = None if colours is not None: self.update(colours) except IOError: return False return True class SessionStore(object): def __init__(self): pass config = Config() session_store = SessionStore() def store_oauth_keys(oauth_token, oauth_token_secret): config["oauth_token"] = oauth_token config["oauth_token_secret"] = oauth_token_secret config.save() identicurse-0.9+dfsg0/src/identicurse/textbox.py0000644000175000017500000003167611720536557021077 0ustar mvdanmvdan# -*- coding: utf-8 -*- # # Copyright (C) 2010-2012 Reality and Psychedelic Squid # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import curses, identicurse, config, helpers, string from curses import textpad from curses import ascii class Textbox(textpad.Textbox): def __init__(self, win, poll, insert_mode=False): try: textpad.Textbox.__init__(self, win, insert_mode) except TypeError: # python 2.5 didn't support insert_mode textpad.Textbox.__init__(self, win) self.poll_function = poll def edit(self, initial_input=""): old_curs_state = 0 try: old_curs_state = curses.curs_set(1) except: pass for char in list(initial_input): self.do_command(ord(char)) self.poll_function(self.count()) abort = False while 1: insert = False ch = self.win.getch() if ch == curses.ascii.DEL: self.do_command(curses.ascii.BS) elif ch == curses.KEY_ENTER or ch == 10: break elif ch == curses.ascii.ESC: abort = True break elif ch == curses.ascii.TAB: cursor_position = self.win.getyx() x = cursor_position[1] last_word = "" while True: x -= 1 c = chr(curses.ascii.ascii(self.win.inch(cursor_position[0], x))) if c == " ": if (len(last_word) == 0) and (x > 0): continue else: break last_word = c + last_word if x == 0: break self.win.move(cursor_position[0], cursor_position[1]) guess_source = None if helpers.url_regex.match(last_word): # this is a URL, shorten it shorturl = helpers.ur1ca_shorten(last_word) for n in xrange(len(last_word)): self.win.move(self.win.getyx()[0], self.win.getyx()[1]-1) self.delch() for char in shorturl: self.do_command(ord(char)) elif last_word[0] == "@" and hasattr(config.session_store, "user_cache"): last_word = last_word[1:] guess_source = getattr(config.session_store, "user_cache") elif last_word[0] == "!" and hasattr(config.session_store, "group_cache"): last_word = last_word[1:] guess_source = getattr(config.session_store, "group_cache") elif last_word[0] == "#" and hasattr(config.session_store, "tag_cache"): last_word = last_word[1:] guess_source = getattr(config.session_store, "tag_cache") elif last_word[0] == "/" and hasattr(config.session_store, "commands"): last_word = last_word[1:] guess_source = getattr(config.session_store, "commands") elif hasattr(config.session_store, "user_cache"): # if no special char, assume username guess_source = getattr(config.session_store, "user_cache") if guess_source is not None: if config.config["tab_complete_mode"] == "exact": possible_guesses = [user for user in guess_source if user[:len(last_word)] == last_word] guess = helpers.find_longest_common_start(possible_guesses) if len(guess) > len(last_word): for char in guess[len(last_word):]: self.do_command(ord(char)) elif len(possible_guesses) > 0: self.poll_function(possible_guesses) else: possible_guesses = helpers.find_fuzzy_matches(last_word, guess_source) common_guess = helpers.find_longest_common_start(possible_guesses) if len(common_guess) != 0 and len(helpers.find_fuzzy_matches(last_word, [common_guess,])) > 0: self.win.move(self.win.getyx()[0], self.win.getyx()[1]-len(last_word)) for i in xrange(len(last_word)): self.delch() for char in common_guess: self.do_command(ord(char)) if len(possible_guesses) >= 2: self.poll_function(possible_guesses) elif ch == curses.KEY_HOME: self.win.move(0, 0) elif ch == curses.KEY_END: for y in range(self.maxy+1): if y == self.maxy: self.win.move(y, self._end_of_line(y)) break if self._end_of_line(y+1) == 0: self.win.move(y, self._end_of_line(y)) break elif ch == curses.KEY_BACKSPACE or ch == curses.ascii.ctrl(ord("h")): cursor_y, cursor_x = self.win.getyx() if cursor_x == 0: if cursor_y == 0: continue else: self.win.move(cursor_y - 1, self.maxx) else: self.win.move(cursor_y, cursor_x - 1) self.delch() elif ch == curses.KEY_DC or ch == curses.ascii.ctrl(ord("d")): self.delch() elif ch == curses.ascii.ctrl(ord("u")): # delete entire line up to the cursor cursor_y, cursor_x = self.win.getyx() self.win.move(cursor_y, 0) for char_count in xrange(0, cursor_x): self.delch() elif ch == curses.ascii.ctrl(ord("w")): # delete all characters before the current one until the beginning of the word cursor_y, cursor_x = self.win.getyx() x, y = cursor_x, cursor_y only_spaces_so_far = True while True: if x == 0: if y == 0: break else: y -= 1 x = self.maxx else: x -= 1 if curses.ascii.ascii(self.win.inch(y, x)) != ord(" "): self.win.move(y, x) self.delch() only_spaces_so_far = False else: if only_spaces_so_far: self.win.move(y, x) self.delch() else: if x == self.maxx: self.win.move(y + 1, 0) else: self.win.move(y, x + 1) break elif ch > 127 and ch <= 256: cursor_y, cursor_x = self.win.getyx() if cursor_y < self.maxy: overhang_ch = self.win.inch(cursor_y, self.maxx) if overhang_ch <= 127: self.win.insch(cursor_y+1, 0, overhang_ch) elif overhang_ch <= 256: for c in self.unicode_demangle(overhang_ch): self.win.insch(cursor_y+1, 0, ord(c)) for c in self.unicode_demangle(ch): self.win.insch(cursor_y, cursor_x, ord(c)) if cursor_x < self.maxx: self.win.move(cursor_y, cursor_x+1) elif cursor_y < self.maxy: self.win.move(cursor_y+1, 0) elif ch <= 127 and chr(ch) in string.printable: cursor_y, cursor_x = self.win.getyx() if cursor_y < self.maxy: overhang_ch = self.win.inch(cursor_y, self.maxx) if overhang_ch <= 127: self.win.insch(cursor_y+1, 0, overhang_ch) elif overhang_ch <= 256: for c in self.unicode_demangle(overhang_ch): self.win.insch(cursor_y+1, 0, ord(c)) self.win.insch(cursor_y, cursor_x, ch) if cursor_x < self.maxx: self.win.move(cursor_y, cursor_x+1) elif cursor_y < self.maxy: self.win.move(cursor_y+1, 0) elif not ch: continue elif not self.do_command(ch): break self.poll_function(self.count()) self.win.refresh() try: curses.curs_set(old_curs_state) # try to restore the cursor's state before returning to normal operation except: pass if abort == False: return self.gather() else: self.win.clear() self.win.refresh() return None def unicode_demangle(self, unicode_ch): #liberated from http://groups.google.com/group/comp.lang.python/browse_thread/thread/67dce30f0a2742a6?fwc=2&pli=1 def check_next_byte(): unicode_ch = self.win.getch() if 128 <= unicode_ch <= 191: return unicode_ch else: raise UnicodeError bytes = [] bytes.append(unicode_ch) if 194 <= unicode_ch <= 223: #2 bytes bytes.append(check_next_byte()) elif 224 <= unicode_ch <= 239: #3 bytes bytes.append(check_next_byte()) bytes.append(check_next_byte()) elif 240 <= unicode_ch <= 244: #4 bytes bytes.append(check_next_byte()) bytes.append(check_next_byte()) bytes.append(check_next_byte()) return "".join([chr(b) for b in bytes]) def delch(self): # delch, but with provisions for moving characters across lines cursor_y, cursor_x = self.win.getyx() self.win.delch() if cursor_y < self.maxy: for line_offset in xrange(0, self.maxy - cursor_y): self.win.insch(cursor_y + line_offset, self.maxx, curses.ascii.ascii(self.win.inch(cursor_y + line_offset + 1, 0))) self.win.delch(cursor_y + line_offset + 1, 0) self.win.move(cursor_y, cursor_x) def gather_only(self): "Collect and return the contents of the window." cursor_position = self.win.getyx() result = [""] for y in range(self.maxy+1): self.win.move(y, 0) stop = self._end_of_line(y) if stop == 0 and self.stripspaces: continue for x in range(self.maxx+1): if self.stripspaces and x > stop: result.append("") break # deal with non-ascii char = self.win.inch(y, x) if char == curses.ascii.ascii(char): char = chr(curses.ascii.ascii(char)) else: char = '&#%u;' % char if char == " ": result.append("") else: result[-1] += char result = [word for word in result if word != ""] self.win.move(*cursor_position) return " ".join(result) def gather(self): "Return the contents of the window, and also clear it. Explicitly use gather_only() if you need to preserve the contents." result = self.gather_only() self.win.clear() self.win.refresh() return result def count(self): cursor_position = self.win.getyx() count = 0 for y in range(self.maxy+1): self.win.move(y, 0) if (y == cursor_position[0]) and (cursor_position[1] > self._end_of_line(y)): stop = cursor_position[1] else: stop = self._end_of_line(y) if stop != 0: count -= 1 else: break for x in range(self.maxx+1): if self.stripspaces and x > stop: break count += 1 self.win.move(cursor_position[0], cursor_position[1]) return count identicurse-0.9+dfsg0/src/identicurse/tabbar.py0000644000175000017500000000622611720536557020626 0ustar mvdanmvdan# -*- coding: utf-8 -*- # # Copyright (C) 2010-2012 Reality and Psychedelic Squid # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import curses, identicurse, config class TabBar(object): def __init__(self, window): self.window = window self.tabs = [] self.current_tab = -1 self.left_index = 0 def update(self): tab_list = [] maxx = self.window.getmaxyx()[1] - 2 total_length = 0 for tab_num in xrange(len(self.tabs)): if tab_num == 0: tab_list.append((" ", identicurse.colour_fields['tabbar'])) total_length += 1 else: tab_list.append((" "*2, identicurse.colour_fields['tabbar'])) total_length += 2 if tab_num == self.current_tab: if (total_length + len(self.tabs[tab_num])) > (maxx + self.left_index): # if this tab would end beyond the right-hand edge self.left_index = (total_length + len(self.tabs[tab_num])) - (maxx - 1) elif total_length < self.left_index: # if this tab would begin beyond the left-hand edge self.left_index = total_length - 1 attr = identicurse.colour_fields['tabbar_active'] else: attr = identicurse.colour_fields['tabbar'] tab_title = self.tabs[tab_num] if config.config["enumerate_tabs"]: tab_title = "%d %s" % (tab_num+1, tab_title) if (not config.config["enable_colours"]) and (tab_num == self.current_tab): tab_list.append((tab_title.upper(), attr)) else: tab_list.append((tab_title, attr)) total_length += len(tab_title) if (self.left_index + maxx) > total_length: if total_length > maxx: # we've still got more than a screen's worth, we just happen to have closed a tab (most likely) self.left_index = (total_length) - (maxx - 1) else: # we're getting the above match because we've got less than a screenfull self.left_index = 0 self.window.erase() char_index = 0 for block in tab_list: for char in block[0]: if char_index < self.left_index: pass elif char_index > (maxx + self.left_index): pass else: self.window.addstr(char, curses.color_pair(block[1])) char_index += 1 self.window.refresh() identicurse-0.9+dfsg0/src/statusnet/0000755000175000017500000000000011720536557016527 5ustar mvdanmvdanidenticurse-0.9+dfsg0/src/statusnet/__init__.py0000644000175000017500000007407111720536557020651 0ustar mvdanmvdan# -*- coding: utf-8 -*- # # Copyright (C) 2010-2012 Reality and Psychedelic Squid # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import urllib, urllib2, httplib, time, re try: from oauth import oauth has_oauth = True except ImportError: has_oauth = False try: import json except ImportError: import simplejson as json domain_regex = re.compile("http(s|)://(www\.|)(.+?)(/.*|)$") def find_split_point(text, width): split_point = width - 1 while True: if split_point == 0: # no smart split point was found, split unsmartly split_point = width - 1 break elif split_point < 0: split_point = 0 break if text[split_point-1] == " ": break else: split_point -= 1 return split_point class StatusNetError(Exception): def __init__(self, errcode, details): self.errcode = errcode self.details = details if errcode == -1: Exception.__init__(self, "Error: %s" % (self.details)) else: Exception.__init__(self, "Error %d: %s" % (self.errcode, self.details)) class StatusNet(object): def __init__(self, api_path, username="", password="", use_auth=True, auth_type="basic", consumer_key=None, consumer_secret=None, oauth_token=None, oauth_token_secret=None, validate_ssl=True, save_oauth_credentials=None): import base64 self.api_path = api_path if self.api_path[-1] == "/": # We don't want a surplus / when creating request URLs. Sure, most servers will handle it well, but why take the chance? self.api_path == self.api_path[:-1] if domain_regex.findall(self.api_path)[0][2] == "api.twitter.com": self.is_twitter = True else: self.is_twitter = False if validate_ssl: #TODO: Implement SSL-validating handler and add it to opener here self.opener = urllib2.build_opener() else: self.opener = urllib2.build_opener() self.use_auth = use_auth self.auth_type = auth_type self.oauth_token = oauth_token self.oauth_token_secret = oauth_token_secret self.save_oauth_credentials = save_oauth_credentials self.auth_string = None if not self.__checkconn(): raise Exception("Couldn't access %s, it may well be down." % (api_path)) if self.use_auth: if auth_type == "basic": self.auth_string = base64.encodestring('%s:%s' % (username, password))[:-1] if self.is_twitter: raise Exception("Twitter does not support basic auth; bailing out.") if not self.account_verify_credentials(): raise Exception("Invalid credentials") elif auth_type == "oauth": if has_oauth: self.consumer = oauth.OAuthConsumer(str(consumer_key), str(consumer_secret)) self.oauth_initialize() if self.is_twitter: self.api_path += "/1" if not self.account_verify_credentials(): raise Exception("OAuth authentication failed") else: raise Exception("OAuth could not be initialised.") self.server_config = self.statusnet_config() try: self.length_limit = int(self.server_config["site"]["textlimit"]) # this will be 0 on unlimited instances except: self.length_limit = 0 # assume unlimited on failure to get a defined limit self.tz = self.server_config["site"]["timezone"] def oauth_initialize(self): if (self.oauth_token is None) or (self.oauth_token_secret is None): # we've never run with oauth before, or we failed, so we'll need to authenticate request_tokens_raw = self.oauth_request_token() request_tokens = {} for item in request_tokens_raw.split("&"): key, value = item.split("=") request_tokens[key] = value verifier = self.oauth_authorize(request_tokens["oauth_token"]) access_tokens_raw = self.oauth_access_token(request_tokens["oauth_token"], request_tokens["oauth_token_secret"], verifier) access_tokens = {} for item in access_tokens_raw.split("&"): key, value = item.split("=") access_tokens[key] = value self.oauth_token = access_tokens['oauth_token'] self.oauth_token_secret = access_tokens['oauth_token_secret'] if self.save_oauth_credentials is not None: self.save_oauth_credentials(self.oauth_token, self.oauth_token_secret) self.token = oauth.OAuthToken(str(self.oauth_token), str(self.oauth_token_secret)) def __makerequest(self, resource_path, raw_params={}, force_get=False): params = urllib.urlencode(raw_params) if not resource_path in ["oauth/request_token", "oauth/access_token"]: resource_path = "%s.json" % (resource_path) if self.auth_type == "basic": if len(raw_params) > 0: if force_get: request = urllib2.Request("%s/%s?%s" % (self.api_path, resource_path, params)) else: request = urllib2.Request("%s/%s" % (self.api_path, resource_path), params) else: request = urllib2.Request("%s/%s" % (self.api_path, resource_path)) if self.auth_string is not None: request.add_header("Authorization", "Basic %s" % (self.auth_string)) elif self.auth_type == "oauth": resource_url = "%s/%s" % (self.api_path, resource_path) if len(raw_params) > 0 and not force_get: oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, self.token, http_method="POST", http_url=resource_url, parameters=raw_params) else: oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, self.token, http_method="GET", http_url=resource_url, parameters=raw_params) oauth_request.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(), self.consumer, self.token) if len(raw_params) > 0 and not force_get: request = urllib2.Request(resource_url, data=oauth_request.to_postdata(), headers=oauth_request.to_header()) else: request = urllib2.Request(oauth_request.to_url(), headers=oauth_request.to_header()) success = False response = None attempt_count = 0 while not success: success = True # succeed unless we hit BadStatusLine if attempt_count >= 10: # after 10 failed attempts raise Exception("Could not successfully read any response. Please check that your connection is working.") try: response = self.opener.open(request) except urllib2.HTTPError, e: raw_details = e.read() try: err_details = json.loads(raw_details)['error'] except ValueError: # not JSON, use raw err_details = raw_details if (e.code % 400) < 100: # only throw the error further up if it's not a server error raise StatusNetError(e.code, err_details) except urllib2.URLError, e: raise StatusNetError(-1, e.reason) except httplib.BadStatusLine, e: success = False attempt_count += 1 if response is None: raise StatusNetError(-1, "Could not successfully read any response. Please check that your connection is working.") content = response.read() try: return json.loads(content) except ValueError: # it wasn't JSON data, return it raw return content def __checkconn(self): try: self.opener.open(self.api_path+"/help/test.json") return True except: return False ############################## # TWITTER-COMPATIBLE METHODS # ############################## ######## Timeline resources ######## def statuses_public_timeline(self): return self.__makerequest("statuses/public_timeline") def statuses_home_timeline(self, since_id=0, max_id=0, count=0, page=0): params = {} if not (since_id == 0): params['since_id'] = since_id if not (max_id == 0): params['max_id'] = max_id if not (count == 0): params['count'] = count if not (page == 0): params['page'] = page return self.__makerequest("statuses/home_timeline", params) def statuses_friends_timeline(self, since_id=0, max_id=0, count=0, page=0, include_rts=False): params = {} if not (since_id == 0): params['since_id'] = since_id if not (max_id == 0): params['max_id'] = max_id if not (count == 0): params['count'] = count if not (page == 0): params['page'] = page if include_rts: params['include_rts'] = "true" return self.__makerequest("statuses/friends_timeline", params) def statuses_mentions(self, since_id=0, max_id=0, count=0, page=0, include_rts=False): params = {} if not (since_id == 0): params['since_id'] = since_id if not (max_id == 0): params['max_id'] = max_id if not (count == 0): params['count'] = count if not (page == 0): params['page'] = page if include_rts: params['include_rts'] = "true" return self.__makerequest("statuses/mentions", params) def statuses_replies(self, since_id=0, max_id=0, count=0, page=0, include_rts=False): # alias of mentions params = {} if not (since_id == 0): params['since_id'] = since_id if not (max_id == 0): params['max_id'] = max_id if not (count == 0): params['count'] = count if not (page == 0): params['page'] = page if include_rts: params['include_rts'] = "true" return self.__makerequest("statuses/replies", params) def statuses_user_timeline(self, user_id=0, screen_name="", since_id=0, max_id=0, count=0, page=0, include_rts=False): params = {} if not (user_id == 0): params['user_id'] = user_id if not (screen_name == ""): params['screen_name'] = screen_name if not (since_id == 0): params['since_id'] = since_id if not (max_id == 0): params['max_id'] = max_id if not (count == 0): params['count'] = count if not (page == 0): params['page'] = page if include_rts: params['include_rts'] = "true" return self.__makerequest("statuses/user_timeline", params, force_get=True) ### StatusNet does not implement this method yet # def statuses_retweeted_by_me(self, since_id=0, max_id=0, count=0, page=0): # params = {} # if not (since_id == 0): # params['since_id'] = since_id # if not (max_id == 0): # params['max_id'] = max_id # if not (count == 0): # params['count'] = count # if not (page == 0): # params['page'] = page # return self.__makerequest("statuses/retweeted_by_me", params) ### StatusNet does not implement this method yet # def statuses_retweeted_to_me(self, since_id=0, max_id=0, count=0, page=0): # params = {} # if not (since_id == 0): # params['since_id'] = since_id # if not (max_id == 0): # params['max_id'] = max_id # if not (count == 0): # params['count'] = count # if not (page == 0): # params['page'] = page # return self.__makerequest("statuses/retweeted_to_me", params) def statuses_retweets_of_me(self, since_id=0, max_id=0, count=0, page=0): params = {} if not (since_id == 0): params['since_id'] = since_id if not (max_id == 0): params['max_id'] = max_id if not (count == 0): params['count'] = count if not (page == 0): params['page'] = page return self.__makerequest("statuses/retweets_of_me", params) ######## Status resources ######## def statuses_show(self, id): return self.__makerequest("statuses/show/%s" % str(id)) def statuses_update(self, status, source="", in_reply_to_status_id=0, latitude=-200, longitude=-200, place_id="", display_coordinates=False, long_dent="split", dup_first_word=False): status = "".join([s.strip(" ") for s in status.split("\n")]) # rejoin split lines back to 1 line params = {'status':status} if not (source == ""): params['source'] = source if not (in_reply_to_status_id == 0): params['in_reply_to_status_id'] = in_reply_to_status_id if not (latitude == -200): params['lat'] = latitude if not (longitude == -200): params['long'] = longitude if not (place_id == ""): params['place_id'] = place_id if display_coordinates: params['display_coordinates'] = "true" if len(status) > self.length_limit and self.length_limit != 0: if long_dent=="truncate": params['status'] = status[:self.length_limit] elif long_dent=="split": status_next = status[find_split_point(status, self.length_limit - 3):] status = status.encode('utf-8')[:find_split_point(status, self.length_limit - 3)] + u".." if dup_first_word: status_next = status.split(" ")[0].encode('utf-8') + " .. " + status_next else: status_next = ".. " + status_next params['status'] = status dents = [self.__makerequest("statuses/update", params)] # post the first piece as normal if in_reply_to_status_id == 0: in_reply_to_status_id = dents[-1]["id"] # if this is not a reply, string everything onto the first dent next_dent = self.statuses_update(status_next, source=source, in_reply_to_status_id=in_reply_to_status_id, latitude=latitude, longitude=longitude, place_id=place_id, display_coordinates=display_coordinates, long_dent=long_dent) # then hand the rest off for potential further splitting if isinstance(next_dent, list): for dent in next_dent: dents.append(dent) else: dents.append(next_dent) else: raise Exception("Maximum status length exceeded by %d characters." % (len(status) - self.length_limit)) return self.__makerequest("statuses/update", params) def statuses_destroy(self, id): params = {'id':id} return self.__makerequest("statuses/destroy", params) def statuses_retweet(self, id, source=""): params = {'id':id} if not (source == ""): params['source'] = source return self.__makerequest("statuses/retweet/%s" % str(id), params) ######## User resources ######## def statuses_friends(self, user_id=0, screen_name="", cursor=0): params = {} if not (user_id == 0): params['user_id'] = user_id if not (screen_name == ""): params['screen_name'] = screen_name if not (cursor == 0): params['cursor'] = cursor return self.__makerequest("statuses/friends", params) def statuses_followers(self, user_id=0, screen_name="", cursor=0): params = {} if not (user_id == 0): params['user_id'] = user_id if not (screen_name == ""): params['screen_name'] = screen_name if not (cursor == 0): params['cursor'] = cursor return self.__makerequest("statuses/followers", params) def users_show(self, user_id=0, screen_name=""): params = {} if not (user_id == 0): params['user_id'] = user_id if not (screen_name == ""): params['screen_name'] = screen_name return self.__makerequest("users/show", params) ######## Direct message resources ######## def direct_messages(self, since_id=0, max_id=0, count=0, page=0): params = {} if not (since_id == 0): params['since_id'] = since_id if not (max_id == 0): params['max_id'] = max_id if not (count == 0): params['count'] = count if not (page == 0): params['page'] = page return self.__makerequest("direct_messages", params) def direct_messages_sent(self, since_id=0, max_id=0, count=0, page=0): params = {} if not (since_id == 0): params['since_id'] = since_id if not (max_id == 0): params['max_id'] = max_id if not (count == 0): params['count'] = count if not (page == 0): params['page'] = page return self.__makerequest("direct_messages/sent", params) def direct_messages_new(self, screen_name, user_id, text, source=""): params = {'screen_name':screen_name, 'user_id':user_id, 'text':text} if not (source == ""): params['source'] = source return self.__makerequest("direct_messages/new", params) # direct_messages/destroy -- NOT IMPLEMENTED BY STATUSNET ######## Friendships resources ######## def friendships_create(self, user_id=0, screen_name=""): params = {} if not (user_id == 0): params['user_id'] = user_id if not (screen_name == ""): params['screen_name'] = screen_name return self.__makerequest("friendships/create", params) def friendships_destroy(self, user_id=0, screen_name=""): params = {} if not (user_id == 0): params['user_id'] = user_id if not (screen_name == ""): params['screen_name'] = screen_name return self.__makerequest("friendships/destroy", params) def friendships_exists(self, user_a, user_b): params = {'user_a':user_a, 'user_b':user_b} return self.__makerequest("friendships/exists", params) def friendships_show(self, source_id=0, source_screen_name="", target_id=0, target_screen_name=""): params = {} if not (source_id == 0): params['source_id'] = source_id if not (source_screen_name == ""): params['source_screen_name'] = source_screen_name if not (target_id == 0): params['target_id'] = target_id if not (target_screen_name == ""): params['target_screen_name'] = target_screen_name return self.__makerequest("friendships/show", params) ######## Friends and followers resources ######## def friends_ids(self, user_id, screen_name, cursor=0): params = {'user_id':user_id, 'screen_name':screen_name} if not (cursor == 0): params['cursor'] = cursor return self.__makerequest("friends/ids", params) def followers_ids(self, user_id, screen_name, cursor=0): params = {'user_id':user_id, 'screen_name':screen_name} if not (cursor == 0): params['cursor'] = cursor return self.__makerequest("followers/ids", params) ######## Account resources ######## def account_verify_credentials(self): try: result = self.__makerequest("account/verify_credentials") return True except: return False # account/end_session -- NOT IMPLEMENTED BY STATUSNET # account/update_location -- IMPLEMENTED, BUT NO DOCUMENTATION # account/update_delivery_device -- NOT IMPLEMENTED BY STATUSNET def account_rate_limit_status(self): return self.__makerequest("account/rate_limit_status") # account/update_profile_background_image - to be implemented if/when we have a helper function for multipart/form-data encoding # account/update_profile_imagee - to be implemented if/when we have a helper function for multipart/form-data encoding ######## Favorite resources ######## def favorites(self, id=0, page=0, since_id=0): params = {} if not (id == 0): params['id'] = id if not (page == 0): params['page'] = page if not (since_id == 0): params['since_id'] = since_id return self.__makerequest("favorites", params) def favorites_create(self, id): params = {'id':id} return self.__makerequest("favorites/create/%d" % (id), params) def favorites_destroy(self, id): params = {'id':id} return self.__makerequest("favorites/destroy/%d" % (id), params) ######## Notification resources ######## # notifications/follow -- NOT IMPLEMENTED BY STATUSNET # notifications/leave -- NOT IMPLEMENTED BY STATUSNET ######## Block resources ######## def blocks_create(self, user_id=0, screen_name=""): params = {} if not (user_id == 0): params['user_id'] = user_id if not (screen_name == ""): params['screen_name'] = screen_name return self.__makerequest("blocks/create", params) def blocks_destroy(self, user_id=0, screen_name=""): params = {} if not (user_id == 0): params['user_id'] = user_id if not (screen_name == ""): params['screen_name'] = screen_name return self.__makerequest("blocks/destroy", params) # blocks/exists -- NOT YET IMPLEMENTED BY STATUSNET # blocks/blocking -- NOT YET IMPLEMENTED BY STATUSNET ######## Help resources ######## def help_test(self): try: return self.__makerequest("help/test") except: return None ######## OAuth resources ######## # will not be implemented unless this module moves to using OAuth instead of basic def oauth_request_token(self): oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, callback="oob", http_method="POST", http_url="%s/%s" % (self.api_path, "oauth/request_token")) oauth_request.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(), self.consumer, None) request = urllib2.Request("%s/%s" % (self.api_path, "oauth/request_token"), data=oauth_request.to_postdata(), headers=oauth_request.to_header()) return self.opener.open(request).read() def oauth_authorize(self, request_token): return raw_input("To authorize IdentiCurse to access your account, you must go to %s/oauth/authorize?oauth_token=%s in your web browser.\nPlease enter the verification code you receive there: " % (self.api_path, request_token)) def oauth_access_token(self, request_token, request_token_secret, verifier): req_token = oauth.OAuthToken(str(request_token), str(request_token_secret)) oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=req_token, verifier=verifier, callback="oob", http_method="POST", http_url="%s/%s" % (self.api_path, "oauth/access_token")) oauth_request.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(), self.consumer, req_token) request = urllib2.Request("%s/%s" % (self.api_path, "oauth/access_token"), data=oauth_request.to_postdata(), headers=oauth_request.to_header()) return self.opener.open(request).read() ######## Search ######## def search(self, query, since_id=0, max_id=0, count=0, page=0, standardise=False): params = {'q':query} if not (since_id == 0): params['since_id'] = since_id if not (max_id == 0): params['max_id'] = max_id if not (count == 0): params['results_per_page'] = count if not (page == 0): params['page'] = page if standardise: # standardise is not part of the API, it is intended to make search results able to be handled as a standard timeline by replacing results with the actual notices as returned by statuses/show results = [self.statuses_show(result['id']) for result in self.__makerequest("search", params)['results']] return results else: return self.__makerequest("search", params) ########################## # STATUSNET-ONLY METHODS # ########################## ######## Group resources ######## def statusnet_groups_timeline(self, group_id=0, nickname="", since_id=0, max_id=0, count=0, page=0): params = {} if not (group_id == 0): params['id'] = group_id if not (nickname == ""): params['nickname'] = nickname if not (since_id == 0): params['since_id'] = since_id if not (max_id == 0): params['max_id'] = max_id if not (count == 0): params['count'] = count if not (page == 0): params['page'] = page if 'id' in params: return self.__makerequest("statusnet/groups/timeline/%d" % (group_id), params) elif 'nickname' in params: return self.__makerequest("statusnet/groups/timeline/%s" % (nickname), params) else: raise Exception("At least one of group_id or nickname must be supplied") def statusnet_groups_show(self, group_id=0, nickname=""): params = {} if not (group_id == 0): params['id'] = group_id if not (nickname == ""): params['nickname'] = nickname if 'id' in params: return self.__makerequest("statusnet/groups/show/%d" % (group_id), params) elif 'nickname' in params: return self.__makerequest("statusnet/groups/show/%s" % (nickname), params) else: raise Exception("At least one of group_id or nickname must be supplied") # statusnet/groups/create -- does not seem to match the proposed API, will leave unimplemented for now def statusnet_groups_join(self, group_id=0, nickname=""): params = {} if not (group_id == 0): params['id'] = group_id if not (nickname == ""): params['nickname'] = nickname if 'id' in params: return self.__makerequest("statusnet/groups/join/%d" % (group_id), params) elif 'nickname' in params: return self.__makerequest("statusnet/groups/join/%s" % (nickname), params) else: raise Exception("At least one of group_id or nickname must be supplied") def statusnet_groups_leave(self, group_id=0, nickname=""): params = {} if not (group_id == 0): params['id'] = group_id if not (nickname == ""): params['nickname'] = nickname if 'id' in params: return self.__makerequest("statusnet/groups/leave/%d" % (group_id), params) elif 'nickname' in params: return self.__makerequest("statusnet/groups/leave/%s" % (nickname), params) else: raise Exception("At least one of group_id or nickname must be supplied") def statusnet_groups_list(self, user_id=0, screen_name=""): params = {} if not (user_id == 0): params['user_id'] = user_id if not (screen_name == ""): params['screen_name'] = screen_name return self.__makerequest("statusnet/groups/list", params) def statusnet_groups_list_all(self, count=0, page=0): params = {} if not (count == 0): params['count'] = count return self.__makerequest("statusnet/groups/list_all", params) def statusnet_groups_membership(self, group_id=0, nickname=""): params = {} if not (group_id == 0): params['id'] = group_id if not (nickname == ""): params['nickname'] = nickname if 'id' in params: return self.__makerequest("statusnet/groups/membership/%d" % (group_id), params) elif 'nickname' in params: return self.__makerequest("statusnet/groups/membership/%s" % (nickname), params) else: raise Exception("At least one of group_id or nickname must be supplied") def statusnet_groups_is_member(self, user_id, group_id): params = {'user_id':user_id, 'group_id':group_id} return self.__makerequest("statusnet/groups/is_member", params)['is_member'] ######## Tag resources ######## def statusnet_tags_timeline(self, tag, since_id=0, max_id=0, count=0, page=0): params = {'tag':tag} if not (since_id == 0): params['since_id'] = since_id if not (max_id == 0): params['max_id'] = max_id if not (count == 0): params['count'] = count if not (page == 0): params['page'] = page return self.__makerequest("statusnet/tags/timeline/%s" % (tag), params) ######## Media resources ######## # statusnet/media/upload - to be implemented if/when we have a helper function for multipart/form-data encoding ######## Conversations ######## def statusnet_conversation(self, id, since_id=0, max_id=0, count=0, page=0): params = {'id': id} if not (since_id == 0): params['since_id'] = since_id if not (max_id == 0): params['max_id'] = max_id if not (count == 0): params['count'] = count if not (page == 0): params['page'] = page return self.__makerequest("statusnet/conversation/%s" % str(id), params, force_get=True) ######## Miscellanea ######## def statusnet_config(self): return self.__makerequest("statusnet/config") def statusnet_version(self): return self.__makerequest("statusnet/version") identicurse-0.9+dfsg0/.gitmodules0000644000175000017500000000013011720536557016055 0ustar mvdanmvdan[submodule "src/oauth"] path = src/oauth url = git://github.com/leah/python-oauth.git identicurse-0.9+dfsg0/conf/0000755000175000017500000000000011720536557014633 5ustar mvdanmvdanidenticurse-0.9+dfsg0/conf/config.json0000644000175000017500000000360011720536557016772 0ustar mvdanmvdan{ "username": "test", "api_path": "https://identi.ca/api", "password": "test", "initial_tabs": "home|mentions|direct|public", "aliases": { "/rt": "/repeat", "/favorites": "/favourites", "/b": "/block", "/gl": "/groupleave", "/fav": "/favourite", "/gj": "/groupjoin", "/gleave": "/groupleave", "/personal": "/home", "/outbox": "/sentdirects", "/find": "/search", "/replies": "/mentions", "/g": "/group", "/f": "/favourite", "/unf": "/unfavourite", "/d": "/direct", "/c": "/context", "/set": "/config", "/fs": "/favourites", "/favs": "/favourites", "/sayagain": "/repeat", "/unb": "/unblock", "/u": "/user", "/t": "/tag", "/s": "/search", "/r": "/reply", "/p": "/profile", "/sr": "/spamreport", "/sub": "/subscribe", "/dm": "/direct", "/nuke": "/spamreport", "/friends": "/home", "/unsub": "/unsubscribe", "/fav": "/favourite", "/unfav": "/unfavourite", "/favorite": "/favourite", "/unfavorite": "/unfavourite", "/?": "/search", "/gjoin": "/groupjoin", "/del": "/delete", "/inbox": "/directs", "/links": "/link *", "/gmember": "/groupmember", "/gm": "/groupmember" }, "update_interval": 60, "notice_limit": 25, "notify": "flash", "long_dent": "split", "browser": "xdg-open '%s'", "keys": { "scrollup": ["k"], "scrolldown": ["j"], "pageup": ["b"], "pagedown": [" "], "scrolltop": ["g"], "scrollbottom": ["G"], "qreply": ["l"], "creply": ["d"], "cfav": ["f"] }, "enable_colours": true } identicurse-0.9+dfsg0/README0000644000175000017500000010116311720536557014570 0ustar mvdanmvdanBASIC CONFIGURATION Before using IdentiCurse, you can manually configure it with your account login credentials. Alternatively, since 0.6, you can simply start IdentiCurse, and let it walk you through a few questions to set up a basic config for you. This will be saved in your home directory as .identicurse/config.json. If you would still rather do it manually (for example, if you would like to configure some of the more advanced settings that the automatic config doesn't touch), you can do so as follows: Edit your config file in your favourite editor, changing "username": "user" and "password": "test" to appropriate values. You can also use IdentiCurse with other StatusNet instances by changing "api_path" to the API home for the instance you wish to use. Important note: IdentiCurse looks for your config file in two locations. First it looks for config.json in .identicurse (a directory which is in your home directory), and then it checks config.json in its installed location. It is highly recommended, if you intend to manually edit it, that you copy the supplied config.json (found in conf/ in the tarball) to $HOME/.identicurse/ (create the directory first if it doesn't exist yet) before modifying this copy, as that way future updates (which *will* overwrite the original config.json with the newest version) will not erase any customisation you have made. Further note: Prior to version 0.8, config was stored in a file called .identicurse in the home directory. This is no longer the case. However, you do not have to do anything about this, as IdentiCurse will move the config to the correct place automatically the first time you run it after updating. This note is just so that you're aware that there has been a change. If you have not used any versions prior to 0.8, you don't need to worry about this section, it doesn't affect you. USING IDENTICURSE Once you've configured IdentiCurse for your account, start it by running the 'identicurse' command. You will see a message, "Welcome to IdentiCurse!", and then after a short while, IdentiCurse itself will load. Once you are at this screen, you can press various keys to do the following: KEY ACTION 1 Switch to tab 1 (initial tab: Personal Timeline). 2 Switch to tab 2 (initial tab: Mentions). 3 Switch to tab 3 (initial tab: DM Inbox). 4 Switch to tab 4 (initial tab: Public Timeline). 5 Switch to tab 5 (no initial tab). 6 Switch to tab 6 (no initial tab). 7 Switch to tab 7 (no initial tab). 8 Switch to tab 8 (no initial tab). 9 Switch to tab 9 (no initial tab). < Switch to the previous tab. > Switch to the next tab. , Move the current tab one place to the left. . Move the current tab one place to the right. x Close the currently visible tab. r Refresh. q Quit IdentiCurse. h Open a tab that displays this README. i Switch to input mode. / Do an in-page search. : Go into insert mode with an initial / already present. n Move to the next match for latest in-page search. N Move to the previous match for latest in-page search. l Followed by a number between 1 and 9, quickly reply to that notice on the timeline. d Reply to currently selected dent, full reply editable. D Reply to currently selected dent, name of the user whose dent you are replying to is added before posting. f Favourite currently selected dent. F Unfavourite currently selected dent. e Repeat currently selected dent. E Quote currently selected dent. c View context for currently selected dent. # Attempt to delete the currently selected dent. s Move dent selection down (indicated by * character). a Move dent selection up (indicated by * character). z Move dent selection to the 1st in the tab (indicated by *). Z Move dent selection to the last in the tab (indicated by *). p Toggle the current tab's pause state. P Toggle all tabs' pause states. m Mute the entire conversation which the currently selected dent is a part of. M Unmute the entire conversation which the currently selected dent is a part of. L Toggle display of notice links (the URLs to the notices). TAB or + Move to the next tab, or to the first if on the last. Shift-TAB or - Move to the previous tab, or to the last if on the first. UP or k Scroll up one line (in the current tab). DOWN or j Scroll down one line (in the current tab). PgUp or b Scroll up one screen (in the current tab). PgDn or SPACE Scroll down one screen (in the current tab). HOME or g Scroll to the top of the current page. END or G Scroll to the bottom of the current page. = Move to the newest page (in current timeline tab). LEFT Move to a newer page (in current timeline tab). RIGHT Move to an older page (in current timeline tab). The secondary keys listed above are configurable, and the choices listed are only the defaults. See the 'ADVANCED CONFIGURATION, Key shortcuts' section for information on how to change them. Also note that key shortcuts are case-sensitive - g indicates a press of the G key without shift, G a press of it with shift. In text entry mode, the above key shortcuts are not available. Instead, you type a message directly into the text entry field, and then press enter to submit. A plain message will simply be posted as a normal dent, but you can also use various commands by starting the message with a / (for example "/reply 1 Hello!" would trigger the /reply command rather than post "/reply 1 Hello!" as a dent). As of 0.7, there are multiple text entry modes. The standard one is Input Mode, and this is the only one where commands work. Commands will *not* work in Reply Mode and Quote Mode, and will instead be posted exactly as entered. Reply Mode and Quote Mode both create dents that are in context of their target dents, the only difference is what text is initially present in the text entry. Available commands are as follows: /reply [notice number] [message] (aliases: /r) This will create a reply to the notice in your current view with the notice number specified. If your current tab is Directs or Sent Directs, the reply will be sent as a DM. /reply [(@)username] [message] (aliases: /r) This will create a mention of the user specified. The username can be entered with or without a @ at the beginning, either will work. If your current tab is Directs or Sent Directs, the reply will be sent as a DM. /reply [notice number] (aliases: /r) This will enter Reply Mode, with the notice indicated by your notice number as its target notice. /favourite [notice number] (aliases: /favorite, /fav, /f) This will add the notice with the notice number specified to your favourites. /unfavourite [notice number] (aliases: /unfavorite, /unfav, /unf) This will remove the notice with the notice number specified from your favourites. /repeat [notice number] (aliases: /rt) This will create a repeat of the notice with the notice number specified. /quote [notice number] This will enter Quote Mode, with the notice indicated by your notice number as its target notice. /direct [(@)username] [message] (aliases: /dm, /d) This will send a direct message to the user specified. As with /reply, the username will work with or without the @. /direct [post number] [message] (aliases: /dm, /d) This will send a direct message to the user who sent the notice or DM with the post number specified. /delete [notice number] (aliases: /del) This will delete the notice with the notice number specified. It will only work for your notices, not those created by other users. /profile [notice number] (aliases: /p) This will open a new tab showing the profile of the user who created the notice with the notice number specified. /profile [(@)username] (aliases: /p) This will open a new tab showing the profile of the user specified. As with /reply and /direct, the @ is optional. /user [notice number] (aliases: /u) This will open a new tab showing the timeline of the user who created the notice with the notice number specified. /user [(@)username] (aliases: /u) This will open a new tab showing the timeline of the user specified. As with /reply and /direct, the @ is optional. /context [notice number] (aliases: /c) This will open a new tab showing the context of the notice with the notice number specified. You can identify notices which have context by the fact that their "from X" message has a [+] after it. /subscribe [notice number] (aliases: /sub) This will subscribe you to the user who created the notice with the notice number specified. /subscribe [(@)username] (aliases: /sub) This will subscribe you to the user specified. The @ is optional. /unsubscribe [notice number] (aliases: /unsub) This will unsubscribe you from the user who created the notice with the notice number specified. /unsubscribe [(@)username] (aliases: /unsub) This will unsubscribe you from the user specified. The @ is optional. /group [(!)group] (aliases: /g) This will open a new tab showing the timeline of the group specified. Much like the @ in username-based commands, the ! is optional. /groupjoin [(!)group] (aliases: /gjoin, /gj) This will add you as a member of the group specified. The ! is optional. /groupleave [(!)group] (aliases: /gleave, /gl) This will remove you from membership of the group specified. The ! is optional. /groupmember [(!)group] (aliases: /gmember, /gm) This will check whether or not you are a member of the group specified. The ! is optional. /tag [(#)tag] (aliases: /t) This will open a new tab showing the timeline of the tag specified. Like the @ or ! in username-/group-based commands, the # is optional. /home (aliases: /personal) This will open a new tab showing your Home (a.k.a., Personal) timeline: that is, notices only from you and people/groups you follow. /mentions (aliases: /replies) This will open a new tab showing notices that mention you. /public This will open a new tab showing the public timeline, which contains the 20 most recent notices from anyone on identi.ca (or whichever instance you are using). /directs (aliases: /inbox) This will open a new tab showing the direct messages other users have sent to you. /sentdirects (aliases: /outbox) This will open a new tab showing the direct messages you have sent to other users. /favourites (aliases: /favorites, /favs, /fs) This will open a new tab showing the direct messages you have added to your favourites. /search [query string] (aliases: /find, /?, /s) This will open a new tab showing notices that contain the query string specified. /block [notice number] (aliases: /b) This will create a block against the user who created the notice with the notice number specified. You can also add additional notice numbers to block the users who created all of them. /block [(@)username] (aliases: /b) This will create a block against of the user specified. As usual, the @ is optional. You can also add additional usernames to block all of them. /unblock [notice number] (aliases: /unb) This will remove a block against the user who created the notice with the notice number specified. You can also add additional notice numbers to unblock the users who created all of them. /unblock [(@)username] (aliases: /unb) This will remove a block against of the user specified. As usual, the @ is optional. You can also add additional usernames to unblock all of them. /spamreport [notice number] [reason] (aliases: /sr, /nuke) This will submit a spam report dent in Identi.ca support's preferred format and also create a block against the user who created the notice with the notice number specified. /spamreport [(@)username] [reason] (aliases: /sr, /nuke) This will submit a spam report dent in Identi.ca support's preferred format and also create a block against the user specified. /link [link number] [notice number] This will open the specified link (numbered starting from 1) from the specified notice in your preferred browser, set in the config file, falling back to xdg-open (which should open your default browser) if you haven't got one specified in the config. You can also use * as the link number to open all links in the notice. There is also an alias for this: "/links [notice number]". (See also 'ADVANCED CONFIGURATION, Link opening' section below.) /link [notice number] As above, but the first link in the notice is selected. /mute [notice number] Mutes the entire conversation to which the chosen notice belongs. This means that any notice in the same conversation will never be shown to you until/unless you unmute the conversation. /unmute [notice number] Removes muting from the entire conversation to which the chosen notice belongs. The exact reverse of the above command. /alias [alias] [command] This will create the alias given as an alias for the command given. For example: /alias /pme /profile @psquid This would make "/pme" an alias for "/profile @psquid" The / before both the alias and the resultant command is optional, as it will be added if it is not present. Therefore, "/alias rpt repeat" and "/alias /rpt /repeat" do exactly the same. /config [key] [value] (aliases: /set) This will set the config item with the specified key to the specified value. The key can also contain .s to indicate subkeys, though so far only aliases require subkeys to configure. Since this isn't such an intuitive command, here are some simple examples: /config aliases./x /delete This would make /x an alias for the /delete command. /config username test This would set your logon username to "test". Note that this particular key is only read in on startup, so changing credentials this way would require a restart of IdentiCurse. /quit This will cause IdentiCurse to quit, exactly the same as if it were quit using the q keybinding. While in text entry mode, you can press tab to attempt to auto-complete the word before the cursor, if it matches any of the following: BEGINS WITH IDENTICURSE WILL TRY TO COMPLETE IT AS A / command @ or no symbol username, unless it's a URL (see below) # tag ! group For commands, all commands are known to IdentiCurse, so if it exists, it can be tab-completed. For usernames, groups and tags, IdentiCurse keeps a cache of all those that it has seen, so any that have not yet been used during the current session will not be available. However, for usernames, the "prefill_user_cache" setting can be set to true (it defaults to false), which will have IdentiCurse, on start-up, fill the username cache with the usernames of everyone you follow. This is somewhat slow, so it is not recommended on slow connections. The exception to this is that if the word before the cursor looks like a URL, IdentiCurse will instead use the ur1.ca service to get a shortened version of the URL, and replace it with that shorter URL. There are two different matching modes for tab-completion, see the "Tab Completion Modes" section of Advanced Configuration for more detail. After submitting your message/command, you will be back in non-text entry mode until you next press i. You can submit an empty text field or press ESC to leave text entry mode without performing any action. ADVANCED CONFIGURATION Update interval: The "update_interval" config setting sets how long, in whole seconds, IdentiCurse should wait after an automatic refresh before starting the next automatic refresh. Notice limit: The "notice_limit" config setting sets how many notices should be fetched per page, on timeline types where the API allows choosing how many notices to send (at the time of writing, most except public do). Length override: The "length_override" config setting sets the minimum number of characters that should be able to fit in the text entry box. So for example, "length_override": 280 would always give a text entry box that can hold 280 characters or more. In practice, it may well give, for example, 300 or so, since the last line will still fill the entire available width, but it cannot give _less than_ 280, so you will be guaranteed the amount you want. This setting is mainly of use on unlimited-length instances, where the text entry box would otherwise be set to 3 lines high, which may be far more or far less than desired. Tab completion modes: The "tab_complete_mode" config setting is used to switch between two tab completion modes. These are: "exact", which will match only possibilities which start with exactly the characters given. For example, "win" would match "wind", but not "gwin". This is the default if the setting is not given. "fuzzy", which will match anything where the characters given all appear in the same order. For example, "wgo" would match "windigo", even though other letters do appear. It would also match "mightywargod", since that still has all the letters, even though it doesn't start with w. Filtering modes: The "filter_mode" config setting is used to switch between two filtering modes. These are: "plain", which will match occurences of exactly the strings present in the "filters" config key. "regex", which will match any notice whose text is matched by the strings present in filters, interpreted as regular expressions. Colours: To use colours, the config setting "enable_colours" must be set to true. This will already be the case if you first started with version 0.6 or later. With only this setting set, a default set of colours will be used. If you want to configure individual colours, you will need to configure the "colours" setting, which uses this format: "colours": { "fieldname": ["fg_colour", "bg_colour"], "fieldname": ["fg_colour", "bg_colour"], "fieldname": ["fg_colour", "bg_colour"], ... } The possible field names are: "statusbar" The status bar. "tabbar" The tab bar, except the active tab. "tabbar_active" The active tab. "timelines" Any part of the timeline view not already dealt with by the fields below. "selector" The '*' current notice indicator. "username" Usernames, both in notice details and within notices themselves. "group" Groups within notices. "tag" Tags within notices. "time" Notice timestamps. "source" Notice sources (e.g. 'from foo'). "notice_count" Notice numbers. "notice" Notice text. "profile_title" The title in profile tabs. "profile_fields" Fields in profile tabs. "profile_values" Values in profile tabs. "search_highlight" Anything on the line of the currently highlighted search result. "notice_link" The links added when show_notice_links is enabled. "warning" Any error/warning messages. "pause_line" The line(s) indicating which dents come before/after a pause. The possible colours differ depending on whether your system's curses library can access 16-colour support or not. You can check by running identicurse with the --colour-check command-line option. For a sizable proportion of terminals, setting your TERM environment variable to "xterm-256color" will give you full colour support. As long as colour is supported at all, the following are usable: "black", "red", "green", "brown", "blue", "magenta", "cyan", "white", and "none" ("none" means that default terminal colours should be used.) If all 16 colours *are* supported, the following are also usable: "grey", "light_red", "light_green", "yellow", "light_blue", "light_magenta", "light_cyan", "light_white" Border: The "border" setting controls whether a border is drawn around the UI. The default is false (no border). UI order: The "ui_order" config setting allows you to place the various sections of the UI in a different vertical order to default. It is in the format of an array of strings, like so: "ui_order": [ "divider", "entry", "divider", "notices", "statusbar", "tabbar" ] The valid UI elements you can use in this setting are as follows: ELEMENT DESCRIPTION entry The text entry field, height determined by notice length. notices The notice window. This will expand to fill all vertical space not taken by other elements. statusbar The status bar. One line high. tabbar The tab bar. One line high. divider An empty line, used for spacing. One line high. Any duplicate elements (except dividers) or unrecognised element names will be ignored, and any element types omitted entirely (again, except dividers) will be added to the bottom. So for example, the example setting given above would produce the same layout with or without the tabbar line, since the tab bar would simply be added there since it was missing. Tab enumeration: The "enumerate_tabs" config setting controls whether tabs are numbered. The default is true (do display numbers). The numbers for tabs 1-9 correspond to the keys that switch to those tabs (clarification: the keys still work regardless of whether numbers are shown in the tab titles). Initial tabs: The tabs that are automatically loaded on startup can be configured by editing the initial_tabs key. This key should contain tab names, separated by vertical bars (|). The valid tab names are as follows: home The personal timeline tab. mentions The mentions tab. direct The received direct messages tab. sentdirect The sent direct messages tab. public The public timeline tab. favourites The favourites tab. user:NAME A user timeline tab for the user with username @NAME. @NAME Same as user:NAME. tag:TAG A tag timeline tab for the #TAG tag. #TAG Same as tag:TAG. group:GROUP A group timeline tab for the !GROUP group. !GROUP Same as group:GROUP. help A help tab, the same as is opened when h is pressed during use. profile:NAME A user profile tab for the user with username @NAME. search:QUERY A search tab with results from searching for QUERY. ?QUERY Same as search:QUERY. context:ID A context tab for the notice with id of ID. Aliases: In addition to the preset aliases, it is possible to add your own custom aliases by editing the "aliases" record in your config file. The preset aliases are stored in this way. and they are good examples of how to correctly format an alias. It is not recommended that you edit this section without a basic understanding of JSON syntax (for a good basic introduction, we recommend CouchDB's JSON Primer: < http://guide.couchdb.org/editions/1/en/json.html >). Long notice handling: When IdentiCurse encounters a notice that is too long to send to the current instance, there are three paths it can take, based on the current value of the long_dent config key: 1 - It simply does not send the notice, instead giving an error indicating how many characters the maximum length was exceeded by. This option is not recommended, as it discards the original message, which must therefore be rewritten from scratch. This will be chosen if long_dent is set to "drop". 2 - The notice is semi-intelligently split into 2 or more notices of sendable length. This will be chosen if long_dent is set to "split". 3 - The notice is truncated, stopping immediately after the last character that fits into the sendable length. This will be chosen if long_dent is set to "truncate". Key shortcuts: When you press a key, IdentiCurse first checks it against its built-in keybindings, then against the bindings set in the config file (falling back to the defaults if you don't have them set). The values to set are all in the "keys" config key, and an example of the format follows: "keys": { "scrollup": ["k"], "scrolldown": ["j"], "refresh": ["l", "m"] } This would make k and j keybindings for scrolling up and scrolling down, respectively, and make *both* l and m keybindings for refreshing. The full range of keys you can set custom bindings for is as follows: KEYNAME ACTION scrollup Scroll up one line. scrolldown Scroll down one line. pageup Scroll up one screen. pagedown Scroll down one screen. scrolltop Scroll right to the top of the current page. scrollbottom Scroll right to the bottom of the current page. firstpage Move to the newest page. newerpage Move to a newer page. olderpage Move to an older page. refresh Refresh. input Go into input mode. commandinput Go into input mode with an initial / already present. quit Quit IdentiCurse. closetab Close the current tab. nexttab Move to the next tab. prevtab Move to the previous tab. tabswapleft Swap the current tab's place with the one to its left. tabswapright Swap the current tab's place with the one to its right. help Show the help. search Start an in-page search. qreply Start a reply to the notice with notice number entered immediately after this key. cfirst Move selected notice to first on current page. clast Move selected notice to last on current page. cnext Move selected notice down. cprev Move selected notice up. creply Reply to selected notice. cfav Favourite selected notice. cunfav Unfavourite selected notice. crepeat Repeat selected notice. ccontext View context for selected notice. creplymode Go into Reply Mode, with selected notice as target. cquote Go into Quote Mode, with selected notice as target. cdelete Delete selected notice. nextmatch Move to next match for in-page search. prevmatch Move to previous match for in-page search. Link opening: When opening a link, IdentiCurse will attempt to use your choice of browser. This is set in the "browser" config key, and should be set as the command to open a URL in the chosen browser, with '%s' instead of the URL, for example: "browser": "firefox '%s'" (which would open URLs in Firefox) GNU Screen users: If you are running IdentiCurse within GNU Screen, the following sample configuration may prove useful. Suppose for example that you want to open URLs/links using the Elinks text browser. In this case the following configuration line should work: "browser": "screen elinks %s" Notice links: The "show_notice_links" config setting, if set to true (default false), adds the web UI link for each notice below it. Compact mode: The "compact_notices" config setting determines whether to display notices in a compact, single-line style (if set to true), or a slightly less compact style (if set to false; this is the default). Show source: The "show_source" config setting, which can be either true or false (true by default), determines whether or not to show which client was used to post each notice. The main use of this is to free up additional screen space in compact mode. Personalised slogans: If you'd rather use your own slogans instead of the built-in ones, you'll need to create a file called .identicurse_slogans in your home directory. In this file, you should place slogans, one per line. These will then be displayed on starting IdentiCurse. The default slogans will not be used in this case, so if there are any you want to keep, you will need to add them to your slogans file. Status bar slogans: As of 0.9, slogans are displayed in the status bar when IdentiCurse is idle. This is controlled by the "status_slogans" config setting, with true (the default) setting them to be shown, and false setting them not to be shown - the traditional "Doing nothing." status will be shown instead. OAuth: If not enabled on first run, OAuth can be enabled at a later date using the "use_oauth" config setting, which can be either true or false. Additionally, the "oauth_token" and "oauth_token_secret" settings hold your OAuth connection details if/when you have successfully used OAuth to connect. To use OAuth with any instance, IdentiCurse must be registered with that instance as an app, which will give it a consumer key/secret that identify it to that instance as being IdentiCurse. As of version 0.7, the consumer key/secret for identi.ca are built in, so anyone using that will not need to read any more of this section. For other instances, IdentiCurse will first check the "consumer_key" and "consumer_secret" config settings, and if that also fails, it will fetch the list at http://identicurse.net/api_keys.json, which is intended to hold all public instance keys that have been registered for IdentiCurse. Also, with each new release, any new keys in http://identicurse.net/api_keys.json will also be added to its local store, so it doesn't have to access identicurse.net for them. If it has to do this, the resulting key/secret will be stored in "consumer_key" and "consumer_secret", so IdentiCurse does not need to fetch the list on future start-ups. If you are using a public instance not already added to that list, please contact @psquid with the name of the instance, and IdentiCurse will be added to it as a valid app, and the consumer key/secret added to the online store at http://identicurse.net/api_keys.json If you are using a private/single-user instance, we will not be able to access it to register IdentiCurse as an app, so you will need to do so yourself, and add the consumer key/secret to your config file manually, in the "consumer_key" and "consumer_secret" settings, as listed above. COMMAND-LINE OPTIONS IdentiCurse takes the following command-line options: -h, --help show the list of available options and exit -c FILE, --config=FILE specify an alternative config file to use -s FILE, --slogans=FILE specify an alternative slogans file to use --colour-check check if system curses library can aceess colours, and how many identicurse-0.9+dfsg0/LICENSE0000644000175000017500000010451311720536557014717 0ustar mvdanmvdan GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . identicurse-0.9+dfsg0/setup.py0000755000175000017500000000354111720536557015426 0ustar mvdanmvdan#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # Copyright (C) 2010-2012 Reality and Psychedelic Squid # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ Standard build script. """ __docformat__ = 'restructuredtext' import distutils from setuptools import setup, find_packages try: distutils.dir_util.remove_tree("build", "dist", "src/identicurse.egg-info") except: pass setup( name="identicurse", version='0.9', description="A simple Identi.ca client with a curses-based UI.", long_description=("A simple Identi.ca client with a curses-based UI."), author="Psychedelic Squid and Reality", author_email='psquid@psquid.net and tinmachin3@gmail.com', url="http://identicurse.net/", download_url=("http://identicurse.net/release/"), license="GPLv3+", data_files=[('identicurse',['README', 'conf/config.json'])], packages=find_packages('src'), package_dir={'': 'src'}, include_package_data=True, entry_points={ 'console_scripts': ['identicurse = identicurse:main'], }, classifiers=[ 'License :: OSI Approved :: GNU General Public License (GPL)', 'Development Status :: 5 - Production/Stable', 'Programming Language :: Python', ], ) identicurse-0.9+dfsg0/build_exe.bat0000644000175000017500000000031111720536557016331 0ustar mvdanmvdanREM This script is used to ensure -OO is added. Also, it is required on 64-bit Vista/7, as running it in XP compatibility mode is needed for a successful build there. python -OO setup_py2exe.py py2exe identicurse-0.9+dfsg0/installer.nsi0000644000175000017500000000202611720536557016416 0ustar mvdanmvdanoutFile "identicurse-0.8-dev_setup.exe" InstallDir $PROGRAMFILES\IdentiCurse Name "IdentiCurse 0.8-dev" section setOutPath $INSTDIR file dist\identicurse.exe file dist\README file dist\config.json createShortCut "$SMPROGRAMS\IdentiCurse.lnk" "$INSTDIR\identicurse.exe" writeUninstaller $INSTDIR\uninstall.exe WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\IdentiCurse" \ "DisplayName" "IdentiCurse" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\IdentiCurse" \ "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\IdentiCurse" \ "DisplayIcon" "$\"$INSTDIR\identicurse.exe$\",0" sectionEnd section "Uninstall" delete $INSTDIR\identicurse.exe delete $INSTDIR\README delete $INSTDIR\config.json delete $INSTDIR\uninstall.exe RMDir $INSTDIR delete $SMPROGRAMS\IdentiCurse.lnk DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\IdentiCurse" sectionEnd