{"action":"created","comment":{"url":"https://api.github.com/repos/mahmoud/boltons/pulls/comments/28484213","id":28484213,"diff_hunk":"@@ -0,0 +1,93 @@\n+\n+\"\"\"Utilities for working with terminals.\n+This supplements the stdlib module `termios`, and is only available for posix platforms.\n+\"\"\"\n+\n+import fcntl\n+import signal\n+import struct\n+import sys\n+import termios\n+\n+__ALL__ = ['TermAttrs', 'termsize']\n+\n+\n+class TermAttrs(object):\n+    def __init__(self, attrs, fd=None, when=termios.TCSANOW):\n+        \"\"\"A context manager for changing terminal attrs.\n+        fd defaults to stdin.\n+        attrs must be a 7-element iterable as taken by tcsetattr.\n+\n+        Note: context manager is not reenterant wrt the same instance.\n+        \"\"\"\n+        self.attrs = attrs\n+        self.fd = fd if fd is not None else sys.stdin.fileno()\n+        self.when = when\n+\n+    @classmethod\n+    def modify(cls, include=(0,0,0,0), exclude=(0,0,0,0), fd=None, *args, **kwargs):\n+        \"\"\"Alternate creation function, allowing you to base changes off current term attrs.\n+        include and exclude should be 4-tuples of (iflag, oflag, cflag, lflag).\n+        All values in include will be set.\n+        All values in exclude will be unset.\n+        All other values will be unchanged from current values.\n+        Other args are passed though to TermAttrs.__init__\n+        \"\"\"\n+        if fd is None: fd = sys.stdin.fileno()\n+        attrs = termios.tcgetattr(fd)\n+        for index, (i, e) in enumerate(zip(include, exclude)):\n+            attrs[index] |= i\n+            attrs[index] &= ~e\n+        return TermAttrs(attrs, fd, *args, **kwargs)\n+\n+    @classmethod\n+    def raw(cls, *args, **kwargs):\n+        \"\"\"Shortcut method for specifying attrs for \"raw mode\" (see termios(3))\"\"\"\n+        t = termios\n+        return cls.modify(\n+            (0, 0, t.CS8, 0),\n+            (t.IGNBRK|t.BRKINT|t.PARMRK|t.ISTRIP|t.INLCR|t.IGNCR|t.IGNNL|t.IXON,\n+             t.OPOST,\n+             t.CSIZE|t.PARENB,\n+             t.ECHO|t.ECHONL|t.ICANON|t.ISIG|t.IEXTEN),\n+        *args, **kwargs)\n+\n+    def __enter__(self):\n+        self.oldattrs = termios.tcgetattr(self.fd)\n+        termios.tcsetattr(self.fd, self.when, self.attrs)\n+\n+    def __exit__(self, *exc_info):\n+        termios.tcsetattr(self.fd, self.when, self.oldattrs)\n+\n+\n+_size_cache = None\n+def termsize(cache=True):\n+    \"\"\"Returns the size (columns, rows) of the terminal associated with sys.stdout.\n+    Note that this function will set up a SIGWINCH handler on first run.\n+    This should be harmless, since it will call any handler that was already registered.\n+    Under normal circumstances, the function will only actually fetch the size on first run\n+    or SIGWINCH. To override this behaviour and force it to refresh the data, pass cache=False.\n+    \"\"\"\n+    old_handler = None\n+\n+    def get_termsize():\n+        winsize_t = 'HHHH'\n+        winsize = struct.pack(winsize_t, 0, 0, 0, 0)\n+        winsize = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, winsize)\n+        rows, columns, _, _ = struct.unpack(winsize_t, winsize)\n+        global _size_cache\n+        _size_cache = columns, rows\n+\n+    def termsize_handler(frame, signal):\n+        get_termsize()\n+        if old_handler is not None and callable(old_handler):\n+            old_handler()\n+\n+    if _size_cache is None:\n+        old_handler = signal.signal(signal.SIGWINCH, termsize_handler)","path":"boltons/termutils.py","position":87,"original_position":87,"commit_id":"69f1c739742db48edb607f51a0e92ac42bec06cd","original_commit_id":"69f1c739742db48edb607f51a0e92ac42bec06cd","user":{"login":"markrwilliams","id":704247,"avatar_url":"https://avatars.githubusercontent.com/u/704247?v=3","gravatar_id":"","url":"https://api.github.com/users/markrwilliams","html_url":"https://github.com/markrwilliams","followers_url":"https://api.github.com/users/markrwilliams/followers","following_url":"https://api.github.com/users/markrwilliams/following{/other_user}","gists_url":"https://api.github.com/users/markrwilliams/gists{/gist_id}","starred_url":"https://api.github.com/users/markrwilliams/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/markrwilliams/subscriptions","organizations_url":"https://api.github.com/users/markrwilliams/orgs","repos_url":"https://api.github.com/users/markrwilliams/repos","events_url":"https://api.github.com/users/markrwilliams/events{/privacy}","received_events_url":"https://api.github.com/users/markrwilliams/received_events","type":"User","site_admin":false},"body":"Unfortunately you can't safely install a signal handler in Pythons prior to 3.5!  Most signals in Python retain their default disposition, which mostly (again) is to terminate the program (see man 7 signal) or to ignore the signal.\r\n\r\nConsequently very little Python code that makes system calls is prepared to handle EINTR. When you set your own handler for some signal, that signal will now cause system calls to terminate with an EINTR error. These two facts together mean that registering signal handlers in Python is serious business!  \r\n\r\nFor example, using this code will mean that if you have a `time.sleep` or a `socket.recv` in your main thread, resizing the terminal will cause that call to fail with EINTR!\r\n\r\nPython 3.5 attempts to resolve this: https://www.python.org/dev/peps/pep-0475/  In prior versions it's better to either not set signal handlers at all, or to only set them as part of some larger event handling system (as Twisted does).","created_at":"2015-04-16T05:07:14Z","updated_at":"2015-04-16T05:07:14Z","html_url":"https://github.com/mahmoud/boltons/pull/17#discussion_r28484213","pull_request_url":"https://api.github.com/repos/mahmoud/boltons/pulls/17","_links":{"self":{"href":"https://api.github.com/repos/mahmoud/boltons/pulls/comments/28484213"},"html":{"href":"https://github.com/mahmoud/boltons/pull/17#discussion_r28484213"},"pull_request":{"href":"https://api.github.com/repos/mahmoud/boltons/pulls/17"}}},"pull_request":{"url":"https://api.github.com/repos/mahmoud/boltons/pulls/17","id":33099110,"html_url":"https://github.com/mahmoud/boltons/pull/17","diff_url":"https://github.com/mahmoud/boltons/pull/17.diff","patch_url":"https://github.com/mahmoud/boltons/pull/17.patch","issue_url":"https://api.github.com/repos/mahmoud/boltons/issues/17","number":17,"state":"open","locked":false,"title":"termutils: New file for termials/termios-related utilities","user":{"login":"ekimekim","id":570052,"avatar_url":"https://avatars.githubusercontent.com/u/570052?v=3","gravatar_id":"","url":"https://api.github.com/users/ekimekim","html_url":"https://github.com/ekimekim","followers_url":"https://api.github.com/users/ekimekim/followers","following_url":"https://api.github.com/users/ekimekim/following{/other_user}","gists_url":"https://api.github.com/users/ekimekim/gists{/gist_id}","starred_url":"https://api.github.com/users/ekimekim/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ekimekim/subscriptions","organizations_url":"https://api.github.com/users/ekimekim/orgs","repos_url":"https://api.github.com/users/ekimekim/repos","events_url":"https://api.github.com/users/ekimekim/events{/privacy}","received_events_url":"https://api.github.com/users/ekimekim/received_events","type":"User","site_admin":false},"body":"I use my computer mainly in a terminal environment, and often find myself needing to do tasks to do with terminal input/output. For example, wanting to turn off echoing and accept input one-char-at-a-time, to implement key-shortcut-controlled interfaces (like htop, mplayer).\r\nOther times, I find myself wanting to know the width/height of the screen for the purposes of displaying output (the COLUMNS env var is not always reliable).\r\n\r\nThe `TermAttrs` class greatly streamlines access to the `termios.tc{get,set}attrs` calls. You always want to reset your termattrs on exit, or else you mess up the terminal for the user's shell. This involves, every time:\r\n```\r\nold_attrs = get_attrs()\r\ntry:\r\n    set_attrs(new_attrs)\r\n    stuff\r\nfinally:\r\n    set_attrs(old_attrs)\r\n```\r\nWith `TermAttrs`, this becomes:\r\n```\r\nwith TermAttrs(new_attrs):\r\n    stuff\r\n```\r\nindeed, this is a textbook example of where a context manager is useful, and it surprised me the stdlib did not implement one.\r\n\r\n`termsize()` caches results and uses SIGWINCH to update. This may be considered by some as \"too invasive\" for a utility library (though I would like to point out that `subprocess` installs a SIGCHLD handler which it requires in order to work correctly) - maybe consider just doing the ioctl every time? Overhead shouldn't be that high, but I kinda designed the function to be called on every \"frame\" of a tight looping interface.","created_at":"2015-04-11T06:59:33Z","updated_at":"2015-04-16T05:07:15Z","closed_at":null,"merged_at":null,"merge_commit_sha":"7647d5d9e5e03ffa49977578030d3401f6a208aa","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/mahmoud/boltons/pulls/17/commits","review_comments_url":"https://api.github.com/repos/mahmoud/boltons/pulls/17/comments","review_comment_url":"https://api.github.com/repos/mahmoud/boltons/pulls/comments{/number}","comments_url":"https://api.github.com/repos/mahmoud/boltons/issues/17/comments","statuses_url":"https://api.github.com/repos/mahmoud/boltons/statuses/69f1c739742db48edb607f51a0e92ac42bec06cd","head":{"label":"ekimekim:termutils","ref":"termutils","sha":"69f1c739742db48edb607f51a0e92ac42bec06cd","user":{"login":"ekimekim","id":570052,"avatar_url":"https://avatars.githubusercontent.com/u/570052?v=3","gravatar_id":"","url":"https://api.github.com/users/ekimekim","html_url":"https://github.com/ekimekim","followers_url":"https://api.github.com/users/ekimekim/followers","following_url":"https://api.github.com/users/ekimekim/following{/other_user}","gists_url":"https://api.github.com/users/ekimekim/gists{/gist_id}","starred_url":"https://api.github.com/users/ekimekim/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ekimekim/subscriptions","organizations_url":"https://api.github.com/users/ekimekim/orgs","repos_url":"https://api.github.com/users/ekimekim/repos","events_url":"https://api.github.com/users/ekimekim/events{/privacy}","received_events_url":"https://api.github.com/users/ekimekim/received_events","type":"User","site_admin":false},"repo":{"id":33706086,"name":"boltons","full_name":"ekimekim/boltons","owner":{"login":"ekimekim","id":570052,"avatar_url":"https://avatars.githubusercontent.com/u/570052?v=3","gravatar_id":"","url":"https://api.github.com/users/ekimekim","html_url":"https://github.com/ekimekim","followers_url":"https://api.github.com/users/ekimekim/followers","following_url":"https://api.github.com/users/ekimekim/following{/other_user}","gists_url":"https://api.github.com/users/ekimekim/gists{/gist_id}","starred_url":"https://api.github.com/users/ekimekim/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ekimekim/subscriptions","organizations_url":"https://api.github.com/users/ekimekim/orgs","repos_url":"https://api.github.com/users/ekimekim/repos","events_url":"https://api.github.com/users/ekimekim/events{/privacy}","received_events_url":"https://api.github.com/users/ekimekim/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/ekimekim/boltons","description":"Like builtins, but boltons. Constructs/recipes/snippets that would be handy in the standard library. Nothing like Michael Bolton.","fork":true,"url":"https://api.github.com/repos/ekimekim/boltons","forks_url":"https://api.github.com/repos/ekimekim/boltons/forks","keys_url":"https://api.github.com/repos/ekimekim/boltons/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ekimekim/boltons/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ekimekim/boltons/teams","hooks_url":"https://api.github.com/repos/ekimekim/boltons/hooks","issue_events_url":"https://api.github.com/repos/ekimekim/boltons/issues/events{/number}","events_url":"https://api.github.com/repos/ekimekim/boltons/events","assignees_url":"https://api.github.com/repos/ekimekim/boltons/assignees{/user}","branches_url":"https://api.github.com/repos/ekimekim/boltons/branches{/branch}","tags_url":"https://api.github.com/repos/ekimekim/boltons/tags","blobs_url":"https://api.github.com/repos/ekimekim/boltons/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ekimekim/boltons/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ekimekim/boltons/git/refs{/sha}","trees_url":"https://api.github.com/repos/ekimekim/boltons/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ekimekim/boltons/statuses/{sha}","languages_url":"https://api.github.com/repos/ekimekim/boltons/languages","stargazers_url":"https://api.github.com/repos/ekimekim/boltons/stargazers","contributors_url":"https://api.github.com/repos/ekimekim/boltons/contributors","subscribers_url":"https://api.github.com/repos/ekimekim/boltons/subscribers","subscription_url":"https://api.github.com/repos/ekimekim/boltons/subscription","commits_url":"https://api.github.com/repos/ekimekim/boltons/commits{/sha}","git_commits_url":"https://api.github.com/repos/ekimekim/boltons/git/commits{/sha}","comments_url":"https://api.github.com/repos/ekimekim/boltons/comments{/number}","issue_comment_url":"https://api.github.com/repos/ekimekim/boltons/issues/comments{/number}","contents_url":"https://api.github.com/repos/ekimekim/boltons/contents/{+path}","compare_url":"https://api.github.com/repos/ekimekim/boltons/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ekimekim/boltons/merges","archive_url":"https://api.github.com/repos/ekimekim/boltons/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ekimekim/boltons/downloads","issues_url":"https://api.github.com/repos/ekimekim/boltons/issues{/number}","pulls_url":"https://api.github.com/repos/ekimekim/boltons/pulls{/number}","milestones_url":"https://api.github.com/repos/ekimekim/boltons/milestones{/number}","notifications_url":"https://api.github.com/repos/ekimekim/boltons/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ekimekim/boltons/labels{/name}","releases_url":"https://api.github.com/repos/ekimekim/boltons/releases{/id}","created_at":"2015-04-10T03:20:55Z","updated_at":"2015-04-11T23:55:06Z","pushed_at":"2015-04-13T11:10:47Z","git_url":"git://github.com/ekimekim/boltons.git","ssh_url":"git@github.com:ekimekim/boltons.git","clone_url":"https://github.com/ekimekim/boltons.git","svn_url":"https://github.com/ekimekim/boltons","homepage":null,"size":754,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"mahmoud:master","ref":"master","sha":"4430b8669b1ba75a1cbe7ab64d75941080aa2636","user":{"login":"mahmoud","id":130193,"avatar_url":"https://avatars.githubusercontent.com/u/130193?v=3","gravatar_id":"","url":"https://api.github.com/users/mahmoud","html_url":"https://github.com/mahmoud","followers_url":"https://api.github.com/users/mahmoud/followers","following_url":"https://api.github.com/users/mahmoud/following{/other_user}","gists_url":"https://api.github.com/users/mahmoud/gists{/gist_id}","starred_url":"https://api.github.com/users/mahmoud/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mahmoud/subscriptions","organizations_url":"https://api.github.com/users/mahmoud/orgs","repos_url":"https://api.github.com/users/mahmoud/repos","events_url":"https://api.github.com/users/mahmoud/events{/privacy}","received_events_url":"https://api.github.com/users/mahmoud/received_events","type":"User","site_admin":false},"repo":{"id":8307391,"name":"boltons","full_name":"mahmoud/boltons","owner":{"login":"mahmoud","id":130193,"avatar_url":"https://avatars.githubusercontent.com/u/130193?v=3","gravatar_id":"","url":"https://api.github.com/users/mahmoud","html_url":"https://github.com/mahmoud","followers_url":"https://api.github.com/users/mahmoud/followers","following_url":"https://api.github.com/users/mahmoud/following{/other_user}","gists_url":"https://api.github.com/users/mahmoud/gists{/gist_id}","starred_url":"https://api.github.com/users/mahmoud/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mahmoud/subscriptions","organizations_url":"https://api.github.com/users/mahmoud/orgs","repos_url":"https://api.github.com/users/mahmoud/repos","events_url":"https://api.github.com/users/mahmoud/events{/privacy}","received_events_url":"https://api.github.com/users/mahmoud/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/mahmoud/boltons","description":"Like builtins, but boltons. Constructs/recipes/snippets that would be handy in the standard library. Nothing like Michael Bolton. ","fork":false,"url":"https://api.github.com/repos/mahmoud/boltons","forks_url":"https://api.github.com/repos/mahmoud/boltons/forks","keys_url":"https://api.github.com/repos/mahmoud/boltons/keys{/key_id}","collaborators_url":"https://api.github.com/repos/mahmoud/boltons/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/mahmoud/boltons/teams","hooks_url":"https://api.github.com/repos/mahmoud/boltons/hooks","issue_events_url":"https://api.github.com/repos/mahmoud/boltons/issues/events{/number}","events_url":"https://api.github.com/repos/mahmoud/boltons/events","assignees_url":"https://api.github.com/repos/mahmoud/boltons/assignees{/user}","branches_url":"https://api.github.com/repos/mahmoud/boltons/branches{/branch}","tags_url":"https://api.github.com/repos/mahmoud/boltons/tags","blobs_url":"https://api.github.com/repos/mahmoud/boltons/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/mahmoud/boltons/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/mahmoud/boltons/git/refs{/sha}","trees_url":"https://api.github.com/repos/mahmoud/boltons/git/trees{/sha}","statuses_url":"https://api.github.com/repos/mahmoud/boltons/statuses/{sha}","languages_url":"https://api.github.com/repos/mahmoud/boltons/languages","stargazers_url":"https://api.github.com/repos/mahmoud/boltons/stargazers","contributors_url":"https://api.github.com/repos/mahmoud/boltons/contributors","subscribers_url":"https://api.github.com/repos/mahmoud/boltons/subscribers","subscription_url":"https://api.github.com/repos/mahmoud/boltons/subscription","commits_url":"https://api.github.com/repos/mahmoud/boltons/commits{/sha}","git_commits_url":"https://api.github.com/repos/mahmoud/boltons/git/commits{/sha}","comments_url":"https://api.github.com/repos/mahmoud/boltons/comments{/number}","issue_comment_url":"https://api.github.com/repos/mahmoud/boltons/issues/comments{/number}","contents_url":"https://api.github.com/repos/mahmoud/boltons/contents/{+path}","compare_url":"https://api.github.com/repos/mahmoud/boltons/compare/{base}...{head}","merges_url":"https://api.github.com/repos/mahmoud/boltons/merges","archive_url":"https://api.github.com/repos/mahmoud/boltons/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/mahmoud/boltons/downloads","issues_url":"https://api.github.com/repos/mahmoud/boltons/issues{/number}","pulls_url":"https://api.github.com/repos/mahmoud/boltons/pulls{/number}","milestones_url":"https://api.github.com/repos/mahmoud/boltons/milestones{/number}","notifications_url":"https://api.github.com/repos/mahmoud/boltons/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/mahmoud/boltons/labels{/name}","releases_url":"https://api.github.com/repos/mahmoud/boltons/releases{/id}","created_at":"2013-02-20T06:17:12Z","updated_at":"2015-04-16T04:55:24Z","pushed_at":"2015-04-15T08:53:46Z","git_url":"git://github.com/mahmoud/boltons.git","ssh_url":"git@github.com:mahmoud/boltons.git","clone_url":"https://github.com/mahmoud/boltons.git","svn_url":"https://github.com/mahmoud/boltons","homepage":"https://boltons.readthedocs.org","size":1822,"stargazers_count":2107,"watchers_count":2107,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":101,"mirror_url":null,"open_issues_count":9,"forks":101,"open_issues":9,"watchers":2107,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/mahmoud/boltons/pulls/17"},"html":{"href":"https://github.com/mahmoud/boltons/pull/17"},"issue":{"href":"https://api.github.com/repos/mahmoud/boltons/issues/17"},"comments":{"href":"https://api.github.com/repos/mahmoud/boltons/issues/17/comments"},"review_comments":{"href":"https://api.github.com/repos/mahmoud/boltons/pulls/17/comments"},"review_comment":{"href":"https://api.github.com/repos/mahmoud/boltons/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/mahmoud/boltons/pulls/17/commits"},"statuses":{"href":"https://api.github.com/repos/mahmoud/boltons/statuses/69f1c739742db48edb607f51a0e92ac42bec06cd"}}}}