summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--poezio/poopt.py181
1 files changed, 181 insertions, 0 deletions
diff --git a/poezio/poopt.py b/poezio/poopt.py
new file mode 100644
index 00000000..016c04e1
--- /dev/null
+++ b/poezio/poopt.py
@@ -0,0 +1,181 @@
+# Copyright 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+#
+# This file is part of Poezio.
+#
+# Poezio is free software: you can redistribute it and/or modify
+# it under the terms of the zlib license. See the COPYING file.
+
+'''This is a template module just for instruction. And poopt.'''
+
+from typing import List, Tuple
+
+# CFFI codepath.
+from cffi import FFI
+
+ffi = FFI()
+ffi.cdef("""
+ typedef long wchar_t;
+ int wcwidth(wchar_t c);
+""")
+libc = ffi.dlopen(None)
+
+# Cython codepath.
+#cdef extern from "wchar.h":
+# ctypedef Py_UCS4 wchar_t
+# int wcwidth(wchar_t c)
+
+# Just checking if the return value is -1. In some (all?) implementations,
+# wcwidth("😆") returns -1 while it should return 1. In these cases, we
+# return 1 instead because this is by far the most probable real value.
+# Since the string is received from python, and the unicode character is
+# extracted with mbrtowc(), and supposing these two compononents are not
+# bugged, and since poezio’s code should never pass '\t', '\n' or their
+# friends, a return value of -1 from wcwidth() is considered to be a bug in
+# wcwidth() (until proven otherwise). xwcwidth() is here to work around
+# this bug.
+def xwcwidth(c: str) -> int:
+ character = ord(c)
+ res = libc.wcwidth(character)
+ if res == -1 and c != '\x19':
+ return 1
+ return res
+
+# cut_text: takes a string and returns a tuple of int.
+#
+# Each two int tuple is a line, represented by the ending position it
+# (where it should be cut). Not that this position is calculed using the
+# position of the python string characters, not just the individual bytes.
+#
+# For example,
+# poopt_cut_text("vivent les réfrigérateurs", 6);
+# will return [(0, 6), (7, 10), (11, 17), (17, 22), (22, 24)], meaning that
+# the lines are
+# "vivent", "les", "réfrig", "érateu" and "rs"
+def cut_text(string: str, width: int) -> List[Tuple[int, int]]:
+ '''cut_text(text, width)
+
+ Return a list of two-tuple, the first int is the starting position of the line and the second is its end.'''
+
+ # The list of tuples that we return
+ retlist = []
+
+ # The start position (in the python-string) of the next line
+ #: unsigned int
+ start_pos = 0
+
+ # The position of the last space seen in the current line. This is used
+ # to cut on spaces instead of cutting inside words, if possible (aka if
+ # there is a space)
+ #: int
+ last_space = -1
+ # The number of columns taken by chars between start_pos and last_space
+ #: size_t
+ cols_until_space = 0
+
+ # Number of columns taken to display the current line so far
+ #: size_t
+ columns = 0
+
+ #: wchar_t
+ #wc = 0
+
+ # The position, considering unicode chars (aka, the position in the
+ # python string). This is used to determine the position in the python
+ # string at which we should cut */
+ #: unsigned int
+ #spos = -1
+
+ in_special_character = False
+ for spos, wc in enumerate(string):
+ # Special case to skip poezio special characters that are contained
+ # in the python string, but should not be counted as chars because
+ # they will not be displayed. Those are the formatting chars (to
+ # insert colors or things like that in the string)
+ if in_special_character:
+ # Skip everything until the end of this format marker, but
+ # without increasing the number of columns of the current
+ # line. Because these chars are not printed.
+ if wc in ('u', 'a', 'i', 'b', 'o', '}'):
+ in_special_character = False
+ continue
+ if wc == '\x19':
+ in_special_character = True
+ continue
+
+ # This is one condition to end the line: an explicit \n is found
+ if wc == '\n':
+ retlist.append((start_pos, spos))
+
+ # And then initiate a new line
+ start_pos = spos
+ last_space = -1
+ columns = 0
+ continue
+
+ # Get the number of columns needed to display this character. May be 0, 1 or 2
+ cols = xwcwidth(wc)
+
+ # This is the second condition to end the line: we have consumed
+ # enough columns to fill a whole line
+ if columns + cols > width:
+ # If possible, cut on a space
+ if last_space != -1:
+ retlist.append((start_pos, last_space))
+ start_pos = last_space + 1
+ last_space = -1
+ columns -= (cols_until_space + 1)
+ else:
+ # Otherwise, cut in the middle of a word
+ retlist.append((start_pos, spos))
+ start_pos = spos
+ columns = 0
+ # We save the position of the last space seen in this line, and the
+ # number of columns we have until now. This helps us keep track of
+ # the columns to count when we will use that space as a cutting
+ # point, later
+ if wc == ' ':
+ last_space = spos
+ cols_until_space = columns
+ # We advanced from one char, increment spos by one and add the
+ # char's columns to the line's columns
+ columns += cols
+ # We are at the end of the string, append the last line, not finished
+ retlist.append((start_pos, spos+1))
+ return retlist
+
+# wcswidth: An emulation of the POSIX wcswidth(3) function using xwcwidth.
+def wcswidth(string: str) -> int:
+ '''wcswidth(s)
+
+ The wcswidth() function returns the number of columns needed to represent the wide-character string pointed to by s. Raise UnicodeError if an invalid unicode value is passed'''
+
+ columns = 0
+ for wc in string:
+ columns += xwcwidth(wc)
+ return columns
+
+# cut_by_columns: takes a python string and a number of columns, returns a
+# python string truncated to take at most that many columns
+# For example cut_by_columns(n, "エメルカ") will return:
+# - n == 5 -> "エメ" (which takes only 4 columns since we can't cut the
+# next character in half)
+# - n == 2 -> "エ"
+# - n == 1 -> ""
+# - n == 42 -> "エメルカ"
+# - etc
+def cut_by_columns(string: str, limit: int) -> str:
+ '''cut_by_columns(string, limit)
+
+ returns a string truncated to take at most limit columns'''
+
+ spos = 0
+ columns = 0
+ for wc in string:
+ if columns == limit:
+ break
+ cols = xwcwidth(wc)
+ if columns + cols > limit:
+ break
+ spos += 1
+ columns += cols
+ return string[:spos]