diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst index a833914a5d56d4..9dd1e14acfd8af 100644 --- a/Doc/library/curses.rst +++ b/Doc/library/curses.rst @@ -2211,6 +2211,17 @@ The :mod:`!curses` module defines the following data members: .. versionadded:: 3.8 +.. data:: pdcurses_version + + A named tuple containing the three components of the PDCurses library + version: *major*, *minor*, and *patch*. All values are integers. The + components can also be accessed by name, so ``curses.pdcurses_version[0]`` + is equivalent to ``curses.pdcurses_version.major`` and so on. + + Availability: if the PDCurses library is used. + + .. versionadded:: next + .. data:: COLORS The maximum number of colors the terminal can support. diff --git a/Include/py_curses.h b/Include/py_curses.h index 85a540086c8384..cd15194c3884b1 100644 --- a/Include/py_curses.h +++ b/Include/py_curses.h @@ -43,6 +43,14 @@ # define PDC_NCMOUSE #endif +/* , which Python.h includes, defines MOUSE_MOVED as a console input + flag; PDCurses gives the name to a mouse event mask of its own. The module + uses the curses one, so drop the Windows definition before the header below + redefines it. */ +#ifdef MOUSE_MOVED +# undef MOUSE_MOVED +#endif + /* On Solaris/illumos, the SVr4 does "typedef char bool;", which clashes with C's bool from . Define _BOOL to suppress it, and include for the bool the header then needs. ncurses ignores diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index f7584a39b182d9..105d86584f0f74 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -29,9 +29,6 @@ except ImportError: pass -# Only reachable once curses imported, so the platform has fcntl too. -import fcntl - def requires_curses_func(name): return unittest.skipUnless(hasattr(curses, name), 'requires curses.%s' % name) @@ -66,6 +63,20 @@ def wrapped(self, *args, **kwargs): test(self, *args, **kwargs) return wrapped +# PDCurses, which the curses module is built against on Windows, differs from +# ncurses in a number of behaviors: its color model, the bit layout of a +# background cell, how it stores a character in a cell, and a few operations +# that report an error. Some tests encode both behaviors, others are skipped. +is_pdcurses = hasattr(curses, 'pdcurses_version') + +# PDCurses writes to the terminal of the process rather than to the streams +# passed to newterm(), and keeps a single screen at a time, which the screen +# the other tests share already occupies. +skip_newterm_on_pdcurses = unittest.skipIf( + is_pdcurses, 'PDCurses ignores the streams passed to newterm()') +skip_on_pdcurses = unittest.skipIf( + is_pdcurses, 'PDCurses behaves differently from ncurses') + term = os.environ.get('TERM') SHORT_MAX = 0x7fff @@ -94,8 +105,9 @@ def _broken_variation_selector_width(): # newterm() is used when available (it reports errors instead of exiting), but # initscr() is still the fallback, and an unusable $TERM has no terminal to -# drive either way. -@unittest.skipIf(not term or term == 'unknown', +# drive either way. PDCurses has no terminfo database and drives the terminal +# of the process, so $TERM says nothing about it. +@unittest.skipIf(not is_pdcurses and (not term or term == 'unknown'), "$TERM=%r, no usable terminal" % term) @unittest.skipIf(sys.platform == "cygwin", "cygwin's curses mostly just hangs") @@ -130,8 +142,10 @@ def setUp(self): self.output = sys.__stderr__ else: try: - # Try to open the terminal device. - tmp = open('/dev/tty', 'wb', buffering=0) + # Open the controlling terminal: the console (CONOUT$) on + # Windows, /dev/tty elsewhere. + tmp = open('CONOUT$' if sys.platform == 'win32' + else '/dev/tty', 'wb', buffering=0) except OSError: # As a fallback, use regular file to write control codes. # Some functions (like savetty) will not work, but at @@ -155,14 +169,30 @@ def setUp(self): # Use newterm() rather than initscr(): it reports errors instead of # exiting, and gives each test a fresh screen, which also lets # ScreenTests run newterm()/set_term() in the same process. - try: - infd = sys.__stdin__.fileno() - if fcntl.fcntl(infd, fcntl.F_GETFL) & os.O_ACCMODE == os.O_WRONLY: - # newterm() needs a readable input fd; a write-only stdin - # (as nohup leaves for a backgrounded run) fails with EINVAL. + if sys.platform == 'win32' and not sys.__stdin__.isatty(): + # PDCurses newterm() needs a real console for input; a piped + # stdin (e.g. a regrtest worker) is not one, so open CONIN$. + try: + conin = open('CONIN$', 'rb', buffering=0) + self.addCleanup(conin.close) + infd = conin.fileno() + except OSError: + infd = stdout_fd + else: + try: + infd = sys.__stdin__.fileno() + try: + import fcntl + except ModuleNotFoundError: + # Windows (curses built against PDCurses) has no fcntl. + pass + else: + if fcntl.fcntl(infd, fcntl.F_GETFL) & os.O_ACCMODE == os.O_WRONLY: + # newterm() needs a readable input fd; a write-only stdin + # (as nohup leaves for a backgrounded run) fails with EINVAL. + infd = stdout_fd + except (AttributeError, ValueError, OSError): infd = stdout_fd - except (AttributeError, ValueError, OSError): - infd = stdout_fd self.screen = curses.newterm(term, stdout_fd, infd) self.stdscr = self.screen.stdscr # Close the screen after the test to break its window<->screen @@ -296,7 +326,9 @@ def test_dupwin(self): sub = win.subwin(3, 5, 2, 3) subdup = sub.dupwin() self.assertEqual(subdup.getmaxyx(), sub.getmaxyx()) - if hasattr(subdup, 'is_subwin'): + # PDCurses keeps the subwindow flag on a duplicated subwindow; ncurses + # makes the copy independent. + if hasattr(subdup, 'is_subwin') and not is_pdcurses: self.assertIs(subdup.is_subwin(), False) self.assertIsNone(subdup.getparent()) @@ -378,20 +410,30 @@ def _encodable(self, s): return True def _storable(self, s): - # Text the current build can place in character cells. A wide build - # stores any locale-encodable text (combining sequences and multibyte - # characters included). A narrow build has no wide-character cells, so - # each character must occupy a single cell -- that is, encode to exactly - # one byte. + # Text the current build can place in character cells. A wide ncurses + # build stores any locale-encodable text (combining sequences and + # multibyte characters included). PDCurses stores one BMP code point + # per cell, so it takes any BMP text whether or not the locale can + # encode it. A narrow build has no wide cells, so each character must + # encode to exactly one byte. + if WIDE_BUILD: + if is_pdcurses: + return all(ord(c) <= 0xffff for c in s) + return self._encodable(s) if not self._encodable(s): return False - if WIDE_BUILD: - return True return len(s.encode(self.stdscr.encoding)) == len(s) def _char_code(self, ch): # The integer the int-input API (addch(int), do_command()) uses for a - # character, or None if it has none: a cell holds a single locale byte. + # character, or None if it has none. A narrow cell holds a single + # locale byte, whichever library it belongs to; a wide PDCurses cell + # holds a code point. + if is_pdcurses and WIDE_BUILD: + code = ord(ch) + if code > curses.A_CHARTEXT or curses.KEY_MIN <= code <= curses.KEY_MAX: + return None + return code try: b = ch.encode(self.stdscr.encoding) except UnicodeEncodeError: @@ -400,10 +442,11 @@ def _char_code(self, ch): def _read_char(self, y, x): # The character written to a cell, read back for output checks. inch() - # is unusable here: on a wide build it returns the low 8 bits of the - # character's code point rather than its locale-encoded byte, mangling - # anything outside Latin-1. in_wch() reads the wide cell directly; - # without it, instr() re-encodes the cell to the window encoding. + # is not used: it returns a chtype whose character is the locale byte on + # ncurses (mangling anything with no single-byte form) and the full code + # point on PDCurses, so it is not a portable way to recover the written + # character. in_wch() reads the wide cell directly; without it (a narrow + # build) instr() gives the cell's bytes in the window encoding. stdscr = self.stdscr if hasattr(stdscr, 'in_wch'): return str(stdscr.in_wch(y, x)) @@ -418,8 +461,10 @@ def test_addch_combining(self): stdscr.addch('e\u0301') # 'e' + COMBINING ACUTE ACCENT if self._encodable('a\u0323\u0300'): stdscr.addch(1, 0, 'a\u0323\u0300') # base plus two combining marks - # Too many code points to fit in a single character cell. - self.assertRaises(TypeError, stdscr.addch, 'e' + '\u0301' * 10) + # Too many code points to fit in a single character cell. The limit + # (CCHARW_MAX) varies by library -- 5 for ncurses, 20 for PDCurses -- + # so use enough combining marks to exceed any of them. + self.assertRaises(TypeError, stdscr.addch, 'e' + '\u0301' * 100) # Only the first code point may be a spacing character. self.assertRaises(ValueError, stdscr.addch, 'ab') self.assertRaises(ValueError, stdscr.addch, 'a\u0301b') @@ -436,7 +481,9 @@ def test_addch_emoji(self): # character plus zero-width combining characters. A lone emoji fits, # as does an emoji with a zero-width variation selector. stdscr = self.stdscr - if self._encodable('\U0001f600'): + # Windows wchar_t is 16 bits, so a character outside the BMP becomes a + # surrogate pair -- two spacing characters -- and cannot share a cell. + if sys.platform != 'win32' and self._encodable('\U0001f600'): stdscr.addch(0, 0, '\U0001f600') # single emoji # Skip the variation selector where the platform reports it as spacing. if not BROKEN_VARIATION_SELECTOR_WIDTH and self._encodable('\u263a\ufe0f'): @@ -530,7 +577,8 @@ def test_complexchar(self): self.assertTrue(cc.attr & curses.A_BOLD) self.assertEqual(cc.pair, 0) # A spacing character optionally followed by combining characters. - if self._storable('e\u0301'): + # PDCurses keeps only the base character of a cell, so skip it there. + if not is_pdcurses and self._storable('e\u0301'): self.assertEqual(str(curses.complexchar('e\u0301')), 'e\u0301') # Defaults: no attributes, color pair 0. cc = curses.complexchar('z') @@ -645,7 +693,8 @@ def test_complexstr(self): self.assertNotEqual(s, curses.complexstr([cc('A'), 'b', cc('c')])) self.assertNotEqual(s, curses.complexstr([cc('A', B), 'b'])) # A spacing character optionally followed by combining characters. - if self._storable('é'): + # PDCurses keeps only the base character of a cell, so skip it there. + if not is_pdcurses and self._storable('é'): self.assertEqual(str(curses.complexstr(['é', 'x'])), 'éx') # cells is positional-only. @@ -668,10 +717,13 @@ def test_complexstr(self): self.assertEqual(len(curses.complexstr(base)), 1) self.assertEqual(curses.complexstr(base)[0], cc(base)) self.assertEqual(len(curses.complexstr('a' + base + 'b')), 3) - # A combining character cannot begin a cell: one that leads the - # string, or overflows a base's combining slots, has no base. + # A combining character cannot begin a cell. The number of + # combining slots varies by library -- 5 for ncurses, 20 for + # PDCurses -- so use enough marks to exceed any; PDCurses reports + # the overflow from setcchar() rather than rejecting the string. self.assertRaises(ValueError, curses.complexstr, '\u0301') - self.assertRaises(ValueError, curses.complexstr, 'e' + '\u0301' * 10) + self.assertRaises((ValueError, curses.error), + curses.complexstr, 'e' + '\u0301' * 100) # A control character may stand alone but not carry combining marks. self.assertRaises(ValueError, curses.complexstr, '\n\u0301') # attr and pair apply to every cell of a string; pair is optional. @@ -780,8 +832,9 @@ def test_output_character(self): # The same characters supplied as an int chtype. The cell is read back # with _read_char(), not inch(): on a wide build the int is stored as a # wide character that inch() cannot represent for a character outside - # Latin-1. The int is decoded as a locale byte, so only a single-byte - # character round-trips. + # Latin-1. PDCurses stores any code point, so the int is ord(c) and the + # whole range round-trips; ncurses decodes the int as a locale byte, so + # only a single-byte character does. for c in ('é', '¤', '€', 'є'): v = self._char_code(c) if v is None: @@ -795,8 +848,7 @@ def test_output_character(self): stdscr.move(2, 0) stdscr.echochar(v) self.assertEqual(self._read_char(2, 0), c) - # insch() decodes the byte through the locale like addch(), so - # it round-trips the same character. + # insch() with an int round-trips the same character as addch(). stdscr.insch(1, 0, v) self.assertEqual(self._read_char(1, 0), c) @@ -966,7 +1018,9 @@ def test_read_from_window(self): self.assertRaises(ValueError, stdscr.instr, 0, 2, -2) # instr(y, x, 1) reads a single cell byte, so only a character that the # window encoding maps to one byte is checked. inch() returns the cell - # value, which is the locale byte. + # value: the locale byte on ncurses, the code point on PDCurses -- these + # differ when the byte is not the code point (e.g. in cp1252 '€' encodes + # to b'\x80' but inch('€')=0x20AC). for ch in ('A', 'é', '¤', '€', 'є'): try: b = ch.encode(stdscr.encoding) @@ -1055,10 +1109,16 @@ def test_getstr(self): self.assertEqual(win.instr(3, 0), b' Lo ipsum ') self.assertEqual(win.getstr(1, 5), b'dolor') self.assertEqual(win.instr(1, 0), b' dolor ') + # The cursor is at (1, 0) from the instr() above, so a getstr() without + # coordinates echoes there on both curses libraries. PDCurses getstr() + # clears to the end of the line as it echoes (ncurses does not), so the + # trailing 'dolor' is erased. self.assertEqual(win.getstr(2), b'si') - self.assertEqual(win.instr(1, 0), b'si dolor ') + self.assertEqual(win.instr(1, 0), + b'si ' if is_pdcurses else b'si dolor ') self.assertEqual(win.getstr(), b'amet') - self.assertEqual(win.instr(1, 0), b'amet dolor ') + self.assertEqual(win.instr(1, 0), + b'amet ' if is_pdcurses else b'amet dolor ') def test_get_wstr(self): # get_wstr() reads input as a str (getstr() returns bytes); feed it with @@ -1278,11 +1338,18 @@ def test_background(self): win.bkgd('#', curses.A_REVERSE) self.assertEqual(win.getbkgd(), b'#'[0] | curses.A_REVERSE) - self.assertEqual(win.inch(0, 0), b'L'[0] | curses.A_REVERSE) - self.assertEqual(win.inch(0, 5), b'#'[0] | curses.A_REVERSE) + if is_pdcurses: + # PDCurses does not merge the background rendition's attributes into + # the cells read back by inch(); only the background character is. + self.assertEqual(win.inch(0, 0), b'L'[0]) + self.assertEqual(win.inch(0, 5), b'#'[0]) + else: + self.assertEqual(win.inch(0, 0), b'L'[0] | curses.A_REVERSE) + self.assertEqual(win.inch(0, 5), b'#'[0] | curses.A_REVERSE) - # A non-ASCII background character reads back as its cell value, the - # locale byte. + # A non-ASCII background character reads back as its cell value: the + # locale byte on ncurses, the code point on PDCurses (getbkgd(), like + # inch(), returns the code point there). win.bkgd(' ') for ch in ('é', '¤', '€', 'є'): v = self._char_code(ch) @@ -1291,9 +1358,9 @@ def test_background(self): with self.subTest(ch=ch): win.bkgd(ch) self.assertEqual(win.getbkgd(), v) - if ord(ch) < 0x100: - # The same byte given as an int. A wide build stores it - # through the locale, so only a Latin-1 byte round-trips. + # The same character given as an int keystroke. ncurses stores + # it through the locale, so only a Latin-1 byte round-trips. + if is_pdcurses or ord(ch) < 0x100: win.bkgd(' ') win.bkgdset(v) self.assertEqual(win.getbkgd(), v) @@ -1489,7 +1556,8 @@ def test_borders_and_lines(self): self.assertEqual(win.inch(3, 1), b'a'[0]) # A border or line character that fits a single cell byte reads back - # via instr() as that byte and via inch() as the cell value. + # via instr() as that byte and via inch() as the cell value: the locale + # byte on ncurses, the code point on PDCurses. for ch in ('é', '¤', '€', 'є'): try: b = ch.encode(win.encoding) @@ -1508,9 +1576,9 @@ def test_borders_and_lines(self): self.assertEqual(win.instr(1, 0, 1), b) win.border(ch, ch, ch, ch, ch, ch, ch, ch) self.assertEqual(win.instr(0, 0), b * maxx) - if ord(ch) < 0x100: - # The same byte given as an int. A wide build stores it - # through the locale, so only a Latin-1 byte round-trips. + # The same character given as an int keystroke. ncurses stores + # it through the locale, so only a Latin-1 byte round-trips. + if is_pdcurses or ord(ch) < 0x100: win.erase() win.hline(2, 0, v, 5) self.assertEqual(win.instr(2, 0, 5), b * 5) @@ -1631,7 +1699,8 @@ def test_env_queries(self): # one, while the narrow variants above return an unspecified byte. try: tty_fd = os.open(os.ctermid(), os.O_RDONLY) - except OSError: + except (AttributeError, OSError): + # No controlling terminal, or no os.ctermid() at all (Windows). tty_fd = None if tty_fd is not None: os.close(tty_fd) @@ -1747,21 +1816,27 @@ def test_state_getters(self): # is_keypad()/is_leaveok() are not available in every curses build. if not hasattr(stdscr, getter): continue + # PDCurses does not track notimeout(). + if is_pdcurses and getter == 'is_notimeout': + continue getattr(stdscr, setter)(True) self.assertIs(getattr(stdscr, getter)(), True) getattr(stdscr, setter)(False) self.assertIs(getattr(stdscr, getter)(), False) # idcok()/idlok() only take effect if the terminal can insert/delete - # characters/lines, so the getter reflects that capability. + # characters/lines, so the getter reflects that capability. PDCurses + # does not track them, so its getter stays False. + idcok_on = False if is_pdcurses else curses.has_ic() + idlok_on = False if is_pdcurses else ( + curses.has_il() or curses.tigetstr('csr') is not None) stdscr.idcok(True) - self.assertIs(stdscr.is_idcok(), curses.has_ic()) + self.assertIs(stdscr.is_idcok(), idcok_on) stdscr.idcok(False) self.assertIs(stdscr.is_idcok(), False) stdscr.idlok(True) - self.assertIs(stdscr.is_idlok(), - curses.has_il() or curses.tigetstr('csr') is not None) + self.assertIs(stdscr.is_idlok(), idlok_on) stdscr.idlok(False) self.assertIs(stdscr.is_idlok(), False) if hasattr(stdscr, 'immedok'): @@ -1912,11 +1987,21 @@ def test_start_color(self): @requires_colors def test_color_content(self): - self.assertEqual(curses.color_content(curses.COLOR_BLACK), (0, 0, 0)) + if is_pdcurses: + # PDCurses reports its own palette, and only for the base colors, so + # just smoke-test that the API returns a valid RGB triple rather than + # the ncurses default values. It reads the live console palette, + # which some consoles (e.g. under ConPTY) do not expose, so a base + # color returning ERR is skipped rather than treated as a failure. + try: + r, g, b = curses.color_content(curses.COLOR_BLACK) + except curses.error: + self.skipTest('color_content() unsupported in this console') + self.assertTrue(0 <= r <= 1000 and 0 <= g <= 1000 and 0 <= b <= 1000) + else: + self.assertEqual(curses.color_content(curses.COLOR_BLACK), (0, 0, 0)) + curses.color_content(curses.COLORS - 1) curses.color_content(0) - maxcolor = curses.COLORS - 1 - curses.color_content(maxcolor) - for color in self.bad_colors(): self.assertRaises(ValueError, curses.color_content, color) @@ -1925,7 +2010,12 @@ def test_init_color(self): if not curses.can_change_color(): self.skipTest('cannot change color') - old = curses.color_content(0) + try: + old = curses.color_content(0) + except curses.error: + if not is_pdcurses: + raise + self.skipTest('color_content() unsupported in this console') try: curses.init_color(0, *old) except curses.error: @@ -1936,12 +2026,15 @@ def test_init_color(self): curses.init_color(0, 1000, 1000, 1000) self.assertEqual(curses.color_content(0), (1000, 1000, 1000)) - maxcolor = curses.COLORS - 1 - old = curses.color_content(maxcolor) - curses.init_color(maxcolor, *old) - self.addCleanup(curses.init_color, maxcolor, *old) - curses.init_color(maxcolor, 0, 500, 1000) - self.assertEqual(curses.color_content(maxcolor), (0, 500, 1000)) + if not is_pdcurses: + # PDCurses can change the base palette but color_content() errors on + # the extended colors. + maxcolor = curses.COLORS - 1 + old = curses.color_content(maxcolor) + curses.init_color(maxcolor, *old) + self.addCleanup(curses.init_color, maxcolor, *old) + curses.init_color(maxcolor, 0, 500, 1000) + self.assertEqual(curses.color_content(maxcolor), (0, 500, 1000)) for color in self.bad_colors(): self.assertRaises(ValueError, curses.init_color, color, 0, 0, 0) @@ -2073,7 +2166,14 @@ def test_use_default_colors(self): curses.use_default_colors() except curses.error: self.skipTest('cannot change color (use_default_colors() failed)') - self.assertEqual(curses.pair_content(0), (-1, -1)) + if is_pdcurses: + # PDCurses has no transparent default color; it reports a concrete + # pair rather than (-1, -1). + fg, bg = curses.pair_content(0) + self.assertIsInstance(fg, int) + self.assertIsInstance(bg, int) + else: + self.assertEqual(curses.pair_content(0), (-1, -1)) @requires_curses_window_meth('use') def test_use_window(self): @@ -2099,6 +2199,14 @@ def test_assume_default_colors(self): curses.assume_default_colors(-1, -1) except curses.error: self.skipTest('cannot change color (assume_default_colors() failed)') + if is_pdcurses: + # PDCurses substitutes concrete colors for the default (-1), so only + # a real color pair round-trips through pair_content(). + curses.assume_default_colors(curses.COLOR_YELLOW, curses.COLOR_BLUE) + self.assertEqual(curses.pair_content(0), + (curses.COLOR_YELLOW, curses.COLOR_BLUE)) + curses.assume_default_colors(-1, -1) + return self.assertEqual(curses.pair_content(0), (-1, -1)) curses.assume_default_colors(curses.COLOR_YELLOW, curses.COLOR_BLUE) self.assertEqual(curses.pair_content(0), (curses.COLOR_YELLOW, curses.COLOR_BLUE)) @@ -2328,71 +2436,68 @@ def test_textbox_fill_last_cell_scrollok(self): self._type(box, 'def') self.assertEqual(box.gather(), 'abc\ndef\n') - def test_textbox_8bit(self): - # An 8-bit-locale character is entered as integer bytes -- the way - # do_command() receives getch() input -- and read back; runs on both - # builds. Run the suite under an 8-bit locale - # (ISO-8859-1, ISO-8859-15 or KOI8-U) to reach the non-ASCII cases; each - # string is used only if the encoding maps it to single bytes. 'abc' is - # ASCII, 'café' is common to the Latin encodings, and the rest are - # distinctive (byte 0xA4 is '¤'/'€'/'є' in ISO-8859-1/-15/KOI8-U). - encoding = self.stdscr.encoding + def test_textbox_int(self): + # A character entered as an integer keystroke -- the way do_command() + # receives getch() input -- and read back; runs on both builds. The + # integer is a locale byte (ncurses) or a code point (PDCurses); run the + # suite under an 8-bit locale (ISO-8859-1, ISO-8859-15 or KOI8-U) to + # reach the non-ASCII cases on a byte build. 'abc' is ASCII, 'café' is + # common to the Latin encodings, and the rest are distinctive (byte 0xA4 + # is '¤'/'€'/'є' in ISO-8859-1/-15/KOI8-U). for text in ['abc', 'café', 'naïve ¤¦', 'café €Šž', 'дякую єі']: try: - data = text.encode(encoding) + data = text.encode(self.stdscr.encoding) except UnicodeEncodeError: continue if len(data) != len(text): - continue # a multibyte encoding is not the 8-bit byte path + continue # do_command() takes one byte per keystroke with self.subTest(text=text): box, win = self._make_textbox(1, 16) - for byte in data: - box.do_command(byte) + for code in data: + box.do_command(code) self.assertEqual(box.gather(), text + ' ') - def test_textbox_8bit_insert(self): + def test_textbox_int_insert(self): # Insert mode shifts the rest of the line right by reading each cell back - # and rewriting it; an 8-bit-locale character entered as bytes must - # survive the shift. See test_textbox_8bit for the character choices. - encoding = self.stdscr.encoding + # and rewriting it; a character entered as an integer keystroke must + # survive the shift. See test_textbox_int for the character choices. for ch in ['é', '¤', '€', 'є']: try: - data = ch.encode(encoding) + data = ('a' + ch + 'c').encode(self.stdscr.encoding) except UnicodeEncodeError: continue - if len(data) != 1: + if len(data) != 3: continue with self.subTest(ch=ch): box, win = self._make_textbox(1, 10, insert_mode=True) - for byte in ('a' + ch + 'c').encode(encoding): - box.do_command(byte) + for code in data: + box.do_command(code) win.move(0, 1) box.do_command(ord('b')) # insert 'b', shifting ch and 'c' right self.assertEqual(box.gather(), 'ab' + ch + 'c ') - def test_textbox_8bit_fill_last_cell(self): - # An 8-bit-locale character entered as bytes must survive being written + def test_textbox_int_fill_last_cell(self): + # A character entered as an integer keystroke must survive being written # to the lower-right cell, which uses insch() rather than addch(). See - # test_textbox_8bit for the character choices. - encoding = self.stdscr.encoding + # test_textbox_int for the character choices. for ch in ['é', '¤', '€', 'є']: + text = 'ab' + ch # the last character fills the corner try: - data = ch.encode(encoding) + data = text.encode(self.stdscr.encoding) except UnicodeEncodeError: continue - if len(data) != 1: + if len(data) != len(text): continue with self.subTest(ch=ch): - text = 'ab' + ch # the last character fills the corner box, win = self._make_textbox(1, len(text), stripspaces=0) - for byte in text.encode(encoding): - box.do_command(byte) + for code in data: + box.do_command(code) self.assertEqual(box.gather(), text) def test_textbox_unicode(self): - # Like test_textbox_8bit, but characters are entered as strings -- the - # way do_command() receives get_wch() input -- rather than integer - # bytes. Each string is used only if encodable in the current locale; + # Like test_textbox_int, but characters are entered as strings -- the + # way do_command() receives get_wch() input -- rather than an integer + # keystroke. Each string is used only if encodable in the current locale; # a narrow build stores one byte per cell, so multi-byte characters # additionally need a wide build. for text in ['abc', 'héšλ', 'café', 'naïve ¤', 'soupçon €Š', 'дякую єі']: @@ -2407,7 +2512,7 @@ def test_textbox_unicode(self): self.assertEqual(box.gather(), text + ' ') def test_textbox_unicode_insert_mode(self): - # Like test_textbox_8bit_insert, but the character is entered as a string + # Like test_textbox_int_insert, but the character is entered as a string # (get_wch() input). Each string is used only if encodable; multi-byte # characters additionally need a wide build (one byte per cell otherwise). for text in ['abcd', 'aβλc', 'aéàc', 'a¤½c', 'a€Šc', 'aдві']: @@ -2583,8 +2688,17 @@ def test_ungetch(self): self.assertEqual(self.stdscr.getkey(), 'C') def test_issue6243(self): - curses.ungetch(1025) - self.stdscr.getkey() + # getkey() must not crash on a key code with no name. On ncurses a code + # past the key range makes keyname() return NULL, and getkey() returns '' + # rather than crashing (bpo-6243). PDCurses' keyname() never returns + # NULL (an unnamed code yields 'UNKNOWN KEY'), and it does not queue a + # code at or beyond KEY_MAX, so use an unnamed in-range code there. + if is_pdcurses: + curses.ungetch(curses.KEY_MAX - 1) + self.assertIsInstance(self.stdscr.getkey(), str) + else: + curses.ungetch(curses.KEY_MAX + 1) + self.assertEqual(self.stdscr.getkey(), '') @unittest.skipIf(getattr(curses, 'ncurses_version', (99,)) < (5, 8), "unget_wch is broken in ncurses 5.7 and earlier") @@ -2844,9 +2958,12 @@ def storable(s): self.assertFalse(curses.ascii.isascii(cc('\xe9'))) self.assertTrue(curses.ascii.ismeta(cc('\xe9'))) self.assertEqual(curses.ascii.ctrl(cc('\xe9')), '\xe9') - # A cell with combining marks is not a single character, so no - # predicate matches it (needs a wide build to store). - if storable('e\u0301'): + # A cell with combining marks is not a single character, so no predicate + # matches it (needs a wide build to store). PDCurses stores a standalone + # complexchar as its base character -- combining marks live in a separate + # table used only when drawing to a window -- so its predicates classify + # the base character instead. + if storable('e\u0301') and not is_pdcurses: self.assertFalse(curses.ascii.isalpha(cc('e\u0301'))) self.assertFalse(curses.ascii.isascii(cc('e\u0301'))) @@ -2948,6 +3065,7 @@ def test_move_down(self): self.mock_win.reset_mock() +@unittest.skipUnless(hasattr(os, 'openpty'), 'requires os.openpty()') class NewtermTestBase(unittest.TestCase): # Shared plumbing for tests that drive newterm() over their own # pseudo-terminal(s). newterm()/set_term() mutate global curses state, but @@ -3008,6 +3126,7 @@ def stop_reader(): return slave +@skip_newterm_on_pdcurses @unittest.skipUnless(hasattr(curses, 'newterm'), 'requires curses.newterm()') @unittest.skipIf(BROKEN_NEWTERM, 'ncurses < 6.5 mishandles repeated newterm()') @unittest.skipIf(not term or term == 'unknown', @@ -3034,6 +3153,8 @@ def test_newterm_file_object(self): screen = curses.newterm(None, out, s) self.assertIsInstance(screen, curses.screen) + @unittest.skipIf(is_pdcurses, + 'PDCurses supports only one screen at a time') def test_set_term(self): s = self.make_pty() s2 = self.make_pty() @@ -3051,6 +3172,8 @@ def test_window_keeps_screen_alive(self): win.addstr(0, 0, 'still alive') win.refresh() + @unittest.skipIf(is_pdcurses, + 'PDCurses supports only one screen at a time') def test_screen_freed(self): # Dropping all references to a (non-current) screen and its windows # frees it without error. @@ -3142,6 +3265,7 @@ def test_disallow_instantiation(self): check_disallow_instantiation(self, curses.screen) +@skip_newterm_on_pdcurses @unittest.skipUnless(hasattr(curses, 'slk_init'), 'requires curses.slk_init()') @unittest.skipUnless(hasattr(curses, 'newterm'), 'requires curses.newterm()') @unittest.skipIf(BROKEN_NEWTERM, 'ncurses < 6.5 mishandles repeated newterm()') @@ -3241,6 +3365,7 @@ def test_color(self): curses.slk_color(0) +@skip_newterm_on_pdcurses @unittest.skipUnless(hasattr(curses, 'newterm'), 'requires curses.newterm()') @unittest.skipIf(BROKEN_NEWTERM, 'ncurses < 6.5 mishandles repeated newterm()') @unittest.skipIf(not term or term == 'unknown', diff --git a/Misc/NEWS.d/next/Windows/2026-07-19-21-30-00.gh-issue-85796.Pd3Cur.rst b/Misc/NEWS.d/next/Windows/2026-07-19-21-30-00.gh-issue-85796.Pd3Cur.rst new file mode 100644 index 00000000000000..374c059842cc49 --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2026-07-19-21-30-00.gh-issue-85796.Pd3Cur.rst @@ -0,0 +1,3 @@ +The :mod:`curses` and :mod:`curses.panel` modules can now be built on Windows +against a user-supplied `PDCurses `_ library by pointing +the ``PDCURSES_DIR`` environment variable at its source tree. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 63e9d1d7a4d95b..32b5d3c80dc030 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -107,6 +107,109 @@ static const char PyCursesVersion[] = "2.2"; #define CURSES_MODULE #include "py_curses.h" +#if defined(MS_WINDOWS) && defined(PDC_WIDE) +# include // setlocale() +#endif + +#ifdef PDCURSES +/* configure does not run on Windows, so declare the capabilities of the + PDCurses library the module is built against. These enable the matching + "#ifdef HAVE_CURSES_X" feature guards below. Only the functions that + PDCurses actually provides are listed -- terminfo (setupterm() and friends) + is supplied by the bundled PC/pdcurses stubs. Functions PDCurses lacks + (the terminfo-based state getters, the reentrant use_window()/use_screen() + family, define_key(), ...) stay disabled because their macros are absent. */ +# ifndef HAVE_TERM_H +# define HAVE_TERM_H 1 +# endif + /* When PDCurses is built with PDC_WIDE it provides the wide-character + (cchar_t) API, which the module gates on HAVE_NCURSESW. PDCurses' + cchar_t is a scalar chtype, not a struct with a combining-character array: + PDCursesMod defines CCHARW_MAX (20), but without a 64-bit chtype a cell + holds a single code point and setcchar() keeps only the base character. */ +# ifdef PDC_WIDE +# define HAVE_NCURSESW 1 + /* The Microsoft C runtime has no wcwidth(); PDCurses carries one as + PDC_wcwidth(), declared in its private curspriv.h and compiled with + PDC_WIDE, so it is there whenever the module needs it. It takes an + int32_t rather than a wchar_t, which on Windows is 16 bits wide. */ +PDCEX int PDC_wcwidth(const int32_t ucs); +# define wcwidth PDC_wcwidth +# endif + /* PDCursesMod defines CCHARW_MAX; PDCurses has no combining-character + support, so a cell holds a single character there. */ +# ifndef CCHARW_MAX +# define CCHARW_MAX 1 +# endif +# define HAVE_CURSES_FILTER 1 + /* py_curses.h asks PDCurses for the ncurses mouse API (PDC_NCMOUSE), which + is the getmouse(MEVENT *) the module calls. */ +# define HAVE_CURSES_GETMOUSE 1 +# define HAVE_CURSES_HAS_KEY 1 +# define HAVE_CURSES_HAS_MOUSE 1 +# define HAVE_CURSES_IS_KEYPAD 1 +# define HAVE_CURSES_IS_LEAVEOK 1 +# define HAVE_CURSES_IS_PAD 1 +# define HAVE_CURSES_RESIZE_TERM 1 +# define HAVE_CURSES_RESIZETERM 1 +# define HAVE_CURSES_IS_TERM_RESIZED 1 +# define HAVE_CURSES_SCR_DUMP 1 +# define HAVE_CURSES_SCR_SET 1 +# define HAVE_CURSES_TABSIZE 1 +# define HAVE_CURSES_SET_TABSIZE 1 +# define HAVE_CURSES_TERM_ATTRS 1 +# define HAVE_CURSES_TYPEAHEAD 1 +# define HAVE_CURSES_USE_ENV 1 +# define HAVE_CURSES_WATTR_GET 1 +# define HAVE_CURSES_WATTR_SET 1 +# define HAVE_CURSES_WATTR_ON 1 +# define HAVE_CURSES_WATTR_OFF 1 +# define HAVE_CURSES_WCOLOR_SET 1 +# define HAVE_CURSES_WCHGAT 1 +# define HAVE_CURSES_SLK_ATTR_ON 1 +# define HAVE_CURSES_SLK_ATTR_OFF 1 +# define HAVE_CURSES_SLK_ATTR_SET 1 +# define HAVE_CURSES_SLK_COLOR 1 + +/* PDCurses provides resize_term() but not the ncurses extensions resizeterm() + and is_term_resized() (neither is in X/Open Curses). resizeterm() is + resize_term() plus SIGWINCH and soft-key bookkeeping, which does not apply on + Windows: a resize is delivered as KEY_RESIZE, which PDCurses' wgetch() already + handles by calling resize_term(0, 0). is_term_resized() is a pure predicate -- + resize_term() changes the window structures exactly when the new size differs + from the current one. */ +static int +resizeterm(int nlines, int ncols) +{ + return resize_term(nlines, ncols); +} + +static int +is_term_resized(int nlines, int ncols) +{ + return nlines != LINES || ncols != COLS; +} +#endif + +/* Capabilities that came in one release rather than one function at a time, + where a version test stands in for the configure probe. */ +#if (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20110404) \ + || defined(PDCURSES) + /* is_cleared() and the other window state predicates, wgetdelay(), + wgetscrreg(). */ +# define HAVE_CURSES_IS_CLEARED 1 +#endif +#if (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20240427) \ + || PDC_BUILD+0 >= 4500 + /* is_cbreak(), is_echo(), is_nl() and is_raw() were added in ncurses 6.5 + and in PDCursesMod 4.5; the original PDCurses does not have them. */ +# define HAVE_CURSES_IS_CBREAK 1 +#endif +#if defined(NCURSES_EXT_FUNCS) || PDC_BUILD+0 >= 4305 + /* slk_attr() is an ncurses extension, added in PDCursesMod 4.3.5. */ +# define HAVE_CURSES_SLK_ATTR 1 +#endif + #if defined(HAVE_TERM_H) || defined(__sgi) /* For termname, longname, putp, tigetflag, tigetnum, tigetstr, tparm which are not declared in SysV curses and for setupterm. */ @@ -131,6 +234,11 @@ typedef chtype attr_t; /* No attr_t type is available */ #if defined(HAVE_NCURSESW) && NCURSES_EXT_FUNCS+0 >= 20170401 && NCURSES_EXT_COLORS+0 >= 20170401 #define _NCURSES_EXTENDED_COLOR_FUNCS 1 +#elif defined(PDCURSES) && PDC_BUILD+0 >= 4300 +// PDCursesMod 4.2 added the int-based extended color API +// (init_extended_pair() etc.), which lifts the color-pair count above the +// short-based limit of SHORT_MAX, and 4.3 added reset_color_pairs(). +#define _NCURSES_EXTENDED_COLOR_FUNCS 1 #else #define _NCURSES_EXTENDED_COLOR_FUNCS 0 #endif @@ -702,6 +810,33 @@ typedef struct { #define _PyCursesComplexStrObject_CAST(op) ((PyCursesComplexStrObject *)(op)) +#ifdef HAVE_NCURSESW + +/* Pack a wide-character cell, routing the color pair through the + extended-color opts slot so it is not limited to a short (unlike the + chtype COLOR_PAIR field). Without that slot the pair must fit in the + short that setcchar() takes; raise OverflowError instead of silently + truncating a larger one. */ +static int +curses_setcchar(cchar_t *wcval, const wchar_t *wstr, attr_t attrs, int pair) +{ +#if _NCURSES_EXTENDED_COLOR_FUNCS + /* The pair passed through the opts slot is authoritative and may exceed + a short; ncurses then ignores the short argument, but clamp it into + range so the int-to-short narrowing stays well-defined. */ + short spair = pair <= SHRT_MAX ? (short)pair : SHRT_MAX; + return setcchar(wcval, wstr, attrs, spair, &pair); +#else + if (pair > SHRT_MAX) { + PyErr_Format(PyExc_OverflowError, + "color pair %d does not fit in a short", pair); + return ERR; + } + return setcchar(wcval, wstr, attrs, (short)pair, NULL); +#endif +} +#endif + /* Build a single character cell from obj. On a wide build, return 1 and store a chtype in *pch for an int or bytes, or @@ -741,7 +876,8 @@ PyCurses_ConvertToCell(PyCursesWindowObject *win, PyObject *obj, attr_t attr, wchar_t wstr[CCHARW_MAX + 1]; int type = PyCurses_ConvertToCchar_t(win, obj, pch, wstr); if (type == 2) { - if (setcchar(pwc, wstr, (attr_t)attr, PAIR_NUMBER(attr), NULL) == ERR) { + if (curses_setcchar(pwc, wstr, (attr_t)attr, + (int)PAIR_NUMBER(attr)) == ERR) { curses_window_set_error(win, "setcchar", funcname); return 0; } @@ -754,30 +890,6 @@ PyCurses_ConvertToCell(PyCursesWindowObject *win, PyObject *obj, attr_t attr, #ifdef HAVE_NCURSESW -/* Pack a wide-character cell, routing the color pair through the - extended-color opts slot so it is not limited to a short (unlike the - chtype COLOR_PAIR field). Without that slot the pair must fit in the - short that setcchar() takes; raise OverflowError instead of silently - truncating a larger one. */ -static int -curses_setcchar(cchar_t *wcval, const wchar_t *wstr, attr_t attrs, int pair) -{ -#if _NCURSES_EXTENDED_COLOR_FUNCS - /* The pair passed through the opts slot is authoritative and may exceed - a short; ncurses then ignores the short argument, but clamp it into - range so the int-to-short narrowing stays well-defined. */ - short spair = pair <= SHRT_MAX ? (short)pair : SHRT_MAX; - return setcchar(wcval, wstr, attrs, spair, &pair); -#else - if (pair > SHRT_MAX) { - PyErr_Format(PyExc_OverflowError, - "color pair %d does not fit in a short", pair); - return ERR; - } - return setcchar(wcval, wstr, attrs, (short)pair, NULL); -#endif -} - /* Unpack a wide-character cell into its text, attributes and color pair. The pair is read through the extended-color opts slot when available, so values above SHRT_MAX are preserved. */ @@ -860,7 +972,7 @@ curses_cell_pack(cursesmodule_state *state, curses_cell_t *cell, only that they are inverses). A wide build, or color_set(), can use larger pairs. */ chtype color = COLOR_PAIR(pair); - if (pair < 0 || PAIR_NUMBER(color) != pair) { + if (pair < 0 || (int)PAIR_NUMBER(color) != pair) { PyErr_Format(PyExc_OverflowError, "%s(): color pair %d does not fit in a chtype " "(color_pair() can encode only pairs 0 to %d)", @@ -1367,7 +1479,7 @@ complexstr_from_string(cursesmodule_state *state, PyObject *str, /* Validate the pair once (it is the same for every cell); see curses_cell_pack() for the round-trip rationale. */ chtype color = COLOR_PAIR(pair); - if (pair < 0 || PAIR_NUMBER(color) != pair) { + if (pair < 0 || (int)PAIR_NUMBER(color) != pair) { PyErr_Format(PyExc_OverflowError, "complexstr(): color pair %d does not fit in a chtype " "(color_pair() can encode only pairs 0 to %d)", @@ -1891,7 +2003,7 @@ Window_NoArgNoReturnFunction(wdeleteln) Window_NoArgTrueFalseFunction(is_wintouched) -#if (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20110404) || defined(PDCURSES) +#ifdef HAVE_CURSES_IS_CLEARED Window_NoArgTrueFalseFunction(is_cleared) Window_NoArgTrueFalseFunction(is_idcok) Window_NoArgTrueFalseFunction(is_idlok) @@ -1902,17 +2014,17 @@ Window_NoArgTrueFalseFunction(is_scrollok) Window_NoArgTrueFalseFunction(is_subwin) Window_NoArgTrueFalseFunction(is_syncok) #endif -#if defined(HAVE_CURSES_IS_KEYPAD) || defined(PDCURSES) +#ifdef HAVE_CURSES_IS_KEYPAD Window_NoArgTrueFalseFunction(is_keypad) #endif -#if defined(HAVE_CURSES_IS_LEAVEOK) || defined(PDCURSES) +#ifdef HAVE_CURSES_IS_LEAVEOK Window_NoArgTrueFalseFunction(is_leaveok) #endif -#if defined(HAVE_CURSES_IS_PAD) || defined(PDCURSES) +#ifdef HAVE_CURSES_IS_PAD Window_NoArgTrueFalseFunction(is_pad) #endif -#if (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20110404) || defined(PDCURSES) +#ifdef HAVE_CURSES_IS_CLEARED static PyObject * PyCursesWindow_getdelay(PyObject *op, PyObject *Py_UNUSED(ignored)) { @@ -1931,7 +2043,7 @@ PyCursesWindow_getscrreg(PyObject *op, PyObject *Py_UNUSED(ignored)) } return Py_BuildValue("(ii)", top, bottom); } -#endif /* NCURSES_EXT_FUNCS >= 20110404 || PDCURSES */ +#endif /* HAVE_CURSES_IS_CLEARED */ static PyObject * PyCursesWindow_getparent(PyObject *op, PyObject *Py_UNUSED(ignored)) @@ -1991,11 +2103,48 @@ PyCursesWindow_New(cursesmodule_state *state, WINDOW *win, const char *encoding, PyCursesWindowObject *orig, PyObject *screen) { +#ifdef PDCURSES + if (encoding == NULL) { + /* PDCurses built with PDC_FORCE_UTF8 encodes a cell as UTF-8 for its + byte API whatever the locale is. Ask at run time: the module may be + compiled without that macro. */ + PDC_VERSION pdcurses_version; + PDC_get_version(&pdcurses_version); + if (pdcurses_version.flags & PDC_VFLAG_UTF8) { + encoding = "utf-8"; + } + } +#endif if (encoding == NULL) { -#if defined(MS_WINDOWS) +#ifdef MS_WINDOWS char buffer[100]; UINT cp; +# ifdef PDC_WIDE + /* On the wide build a cell holds a Unicode code point and output goes + to the console as Unicode (WriteConsoleW), so the console output code + page is not involved. The byte API -- instr(), addstr(bytes) -- goes + through PDCurses' wcstombs()/mbtowc(), which use the C runtime's + current LC_CTYPE code page -- the locale the program set -- not the + system ANSI code page. */ + cp = 0; + /* setlocale(LC_CTYPE, NULL) reports the CURRENT locale (per-thread on + this CRT), matching what wcstombs() uses; parse its code page. */ + const char *lc = setlocale(LC_CTYPE, NULL); + const char *dot = (lc != NULL) ? strrchr(lc, '.') : NULL; + if (dot == NULL) { + /* the "C" locale (no code page): wcstombs() maps a wide character + below 256 to its own byte value, i.e. Latin-1. */ + encoding = "latin-1"; + } + else if (_stricmp(dot + 1, "utf8") == 0 || strcmp(dot + 1, "65001") == 0) { + encoding = "utf-8"; + } + else { + cp = (UINT)atoi(dot + 1); + } +# else cp = GetConsoleOutputCP(); +# endif if (cp != 0) { PyOS_snprintf(buffer, sizeof(buffer), "cp%u", cp); encoding = buffer; @@ -3070,7 +3219,7 @@ _curses_window_echochar_impl(PyCursesWindowObject *self, PyObject *ch, return curses_window_check_err(self, rtn, funcname, "echochar"); } -#if defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES) +#ifdef HAVE_CURSES_GETMOUSE /*[clinic input] @permit_long_summary _curses.window.enclose @@ -3638,9 +3787,11 @@ _curses_window_insch_impl(PyCursesWindowObject *self, int group_left_1, return NULL; } if (type == 1) { - /* winsch() does not locale-decode a byte above 127 on a wide build, - unlike waddch(), so decode it here and insert it as a wide - character. (gh-153864) */ +#ifndef PDCURSES + /* ncurses winsch() does not locale-decode a byte above 127 on a wide + build, unlike waddch(), so decode it here and insert it as a wide + character. (gh-153864) PDCurses winsch() stores the value as a code + point (like waddch()), so it needs no decoding. */ chtype cch = ch_ & A_CHARTEXT; if (cch > 127) { wint_t wc = btowc((int)cch); @@ -3654,6 +3805,7 @@ _curses_window_insch_impl(PyCursesWindowObject *self, int group_left_1, type = 2; } } +#endif } if (type == 2) { if (!group_left_1) { @@ -3707,9 +3859,10 @@ _curses_window_inch_impl(PyCursesWindowObject *self, int group_right_1, { chtype rtn; const char *funcname; -#ifdef HAVE_NCURSESW +#if defined(HAVE_NCURSESW) && !defined(PDCURSES) /* ncursesw's winch() returns the whole code point, which overflows the - chtype's 8-bit character field into the color and attribute bits. */ + chtype's 8-bit character field into the color and attribute bits. + PDCurses stores a code point in a cell, so its winch() is correct. */ cchar_t cell = {0}; int rc; if (!group_right_1) { @@ -5046,7 +5199,7 @@ static PyMethodDef PyCursesWindow_methods[] = { _CURSES_WINDOW_GETCH_METHODDEF _CURSES_WINDOW_GETKEY_METHODDEF _CURSES_WINDOW_GET_WCH_METHODDEF -#if (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20110404) || defined(PDCURSES) +#ifdef HAVE_CURSES_IS_CLEARED {"getdelay", PyCursesWindow_getdelay, METH_NOARGS, "getdelay($self, /)\n--\n\n" "Return the window's read timeout in milliseconds.\n\n" @@ -5061,7 +5214,7 @@ static PyMethodDef PyCursesWindow_methods[] = { {"getparyx", PyCursesWindow_getparyx, METH_NOARGS, "getparyx($self, /)\n--\n\n" "Return (y, x) relative to the parent window, or (-1, -1) if none."}, -#if (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20110404) || defined(PDCURSES) +#ifdef HAVE_CURSES_IS_CLEARED {"getscrreg", PyCursesWindow_getscrreg, METH_NOARGS, "getscrreg($self, /)\n--\n\n" "Return a tuple (top, bottom) of the current scrolling region."}, @@ -5116,7 +5269,7 @@ static PyMethodDef PyCursesWindow_methods[] = { {"is_wintouched", PyCursesWindow_is_wintouched, METH_NOARGS, "is_wintouched($self, /)\n--\n\n" "Return True if the window changed since the last refresh()."}, -#if (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20110404) || defined(PDCURSES) +#ifdef HAVE_CURSES_IS_CLEARED {"is_cleared", PyCursesWindow_is_cleared, METH_NOARGS, "is_cleared($self, /)\n--\n\n" "Return the current value set by clearok()."}, @@ -5145,17 +5298,17 @@ static PyMethodDef PyCursesWindow_methods[] = { "is_syncok($self, /)\n--\n\n" "Return the current value set by syncok()."}, #endif -#if defined(HAVE_CURSES_IS_KEYPAD) || defined(PDCURSES) +#ifdef HAVE_CURSES_IS_KEYPAD {"is_keypad", PyCursesWindow_is_keypad, METH_NOARGS, "is_keypad($self, /)\n--\n\n" "Return the current value set by keypad()."}, #endif -#if defined(HAVE_CURSES_IS_LEAVEOK) || defined(PDCURSES) +#ifdef HAVE_CURSES_IS_LEAVEOK {"is_leaveok", PyCursesWindow_is_leaveok, METH_NOARGS, "is_leaveok($self, /)\n--\n\n" "Return the current value set by leaveok()."}, #endif -#if defined(HAVE_CURSES_IS_PAD) || defined(PDCURSES) +#ifdef HAVE_CURSES_IS_PAD {"is_pad", PyCursesWindow_is_pad, METH_NOARGS, "is_pad($self, /)\n--\n\n" "Return True if the window is a pad."}, @@ -5645,8 +5798,7 @@ _curses_cbreak_impl(PyObject *module, int flag) /*[clinic end generated code: output=9f9dee9664769751 input=42d81687f11ddbf3]*/ NoArgOrFlagNoReturnFunctionBody(cbreak, flag) -/* is_cbreak()/is_echo()/is_nl()/is_raw() were added in ncurses 6.5. */ -#if (defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20240427) || defined(PDCURSES) +#ifdef HAVE_CURSES_IS_CBREAK /*[clinic input] _curses.is_cbreak @@ -5702,7 +5854,7 @@ _curses_is_raw_impl(PyObject *module) PyCursesStatefulInitialised(module); return PyBool_FromLong(is_raw()); } -#endif /* NCURSES_EXT_FUNCS >= 20240427 || PDCURSES */ +#endif /* HAVE_CURSES_IS_CBREAK */ /*[clinic input] _curses.color_content @@ -5763,7 +5915,7 @@ _curses_color_pair_impl(PyObject *module, int pair_number) macros are inverses). color_set()/attr_set()/complexchar can still display larger pairs. */ chtype attr = COLOR_PAIR(pair_number); - if (pair_number < 0 || PAIR_NUMBER(attr) != pair_number) { + if (pair_number < 0 || (int)PAIR_NUMBER(attr) != pair_number) { PyErr_Format(PyExc_OverflowError, "color pair %d does not fit in a chtype " "(color_pair() can encode only pairs 0 to %d)", @@ -6006,7 +6158,7 @@ _curses_getsyx_impl(PyObject *module) } #endif -#if defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES) +#ifdef HAVE_CURSES_GETMOUSE /*[clinic input] _curses.getmouse @@ -7238,8 +7390,8 @@ _curses_meta_impl(PyObject *module, int yes) return curses_check_err(module, meta(stdscr, yes), "meta", NULL); } -#if defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES) -#if defined(HAVE_CURSES_HAS_MOUSE) || defined(PDCURSES) +#ifdef HAVE_CURSES_GETMOUSE +#ifdef HAVE_CURSES_HAS_MOUSE /*[clinic input] _curses.has_mouse @@ -7254,7 +7406,7 @@ _curses_has_mouse_impl(PyObject *module) return PyBool_FromLong(has_mouse()); } -#endif /* HAVE_CURSES_HAS_MOUSE || PDCURSES */ +#endif /* HAVE_CURSES_HAS_MOUSE */ /*[clinic input] _curses.mouseinterval @@ -7901,7 +8053,7 @@ _curses_termattrs_impl(PyObject *module) { PyCursesStatefulInitialised(module); - return PyLong_FromUnsignedLong((unsigned long)(chtype)termattrs()); + return PyLong_FromUnsignedLongLong((unsigned long long)(chtype)termattrs()); } #ifdef HAVE_CURSES_TERM_ATTRS @@ -8236,7 +8388,7 @@ _curses_ungetch(PyObject *module, PyObject *ch) if (!PyCurses_ConvertToChtype(NULL, ch, &ch_)) return NULL; - return curses_check_err(module, ungetch(ch_), "ungetch", NULL); + return curses_check_err(module, ungetch((int)ch_), "ungetch", NULL); } /*[clinic input] @@ -8491,7 +8643,7 @@ _curses_slk_attrset_impl(PyObject *module, attr_t attr) "slk_attrset", NULL); } -#if defined(NCURSES_EXT_FUNCS) || defined(PDCURSES) +#ifdef HAVE_CURSES_SLK_ATTR /*[clinic input] _curses.slk_attr @@ -8670,60 +8822,82 @@ _curses_assume_default_colors_impl(PyObject *module, int fg, int bg) #endif /* STRICT_SYSV_CURSES */ -#ifdef NCURSES_VERSION +/* Only one curses library is compiled against, so a single named tuple + reports its version, named after that library. Both ncurses and PDCurses + provide curses_version(), with version strings the same scan handles. */ +#if defined(NCURSES_VERSION) || defined(PDCURSES) -PyDoc_STRVAR(ncurses_version__doc__, -"curses.ncurses_version\n\ +#ifdef NCURSES_VERSION +# define CURSES_LIB_NAME "ncurses" +# define CURSES_LIB_VERSION_ATTR "ncurses_version" +# define CURSES_LIB_VERSION_MAJOR NCURSES_VERSION_MAJOR +# define CURSES_LIB_VERSION_MINOR NCURSES_VERSION_MINOR +# define CURSES_LIB_VERSION_PATCH NCURSES_VERSION_PATCH +#else +# define CURSES_LIB_NAME "PDCurses" +# define CURSES_LIB_VERSION_ATTR "pdcurses_version" +# define CURSES_LIB_VERSION_MAJOR PDC_VER_MAJOR +# define CURSES_LIB_VERSION_MINOR PDC_VER_MINOR +# ifdef PDC_VER_CHANGE +# define CURSES_LIB_VERSION_PATCH PDC_VER_CHANGE +# else + /* PDCurses numbers its releases with two components. */ +# define CURSES_LIB_VERSION_PATCH 0 +# endif +#endif + +PyDoc_STRVAR(curses_lib_version__doc__, +"curses." CURSES_LIB_VERSION_ATTR "\n\ \n\ -Ncurses version information as a named tuple."); +" CURSES_LIB_NAME " version information as a named tuple."); -static PyStructSequence_Field ncurses_version_fields[] = { +static PyStructSequence_Field curses_lib_version_fields[] = { {"major", "Major release number"}, {"minor", "Minor release number"}, {"patch", "Patch release number"}, {0} }; -static PyStructSequence_Desc ncurses_version_desc = { - "curses.ncurses_version", /* name */ - ncurses_version__doc__, /* doc */ - ncurses_version_fields, /* fields */ +static PyStructSequence_Desc curses_lib_version_desc = { + "curses." CURSES_LIB_VERSION_ATTR, /* name */ + curses_lib_version__doc__, /* doc */ + curses_lib_version_fields, /* fields */ 3 }; static PyObject * -make_ncurses_version(PyTypeObject *type) +make_curses_lib_version(PyTypeObject *type) { - PyObject *ncurses_version = PyStructSequence_New(type); - if (ncurses_version == NULL) { + PyObject *lib_version = PyStructSequence_New(type); + if (lib_version == NULL) { return NULL; } const char *str = curses_version(); unsigned long major = 0, minor = 0, patch = 0; if (!str || sscanf(str, "%*[^0-9]%lu.%lu.%lu", &major, &minor, &patch) < 3) { // Fallback to header version, which cannot be that wrong - major = NCURSES_VERSION_MAJOR; - minor = NCURSES_VERSION_MINOR; - patch = NCURSES_VERSION_PATCH; + major = CURSES_LIB_VERSION_MAJOR; + minor = CURSES_LIB_VERSION_MINOR; + patch = CURSES_LIB_VERSION_PATCH; } #define SET_VERSION_COMPONENT(INDEX, VALUE) \ do { \ PyObject *o = PyLong_FromLong(VALUE); \ if (o == NULL) { \ - Py_DECREF(ncurses_version); \ + Py_DECREF(lib_version); \ return NULL; \ } \ - PyStructSequence_SET_ITEM(ncurses_version, INDEX, o); \ + PyStructSequence_SET_ITEM(lib_version, INDEX, o); \ } while (0) SET_VERSION_COMPONENT(0, major); SET_VERSION_COMPONENT(1, minor); SET_VERSION_COMPONENT(2, patch); #undef SET_VERSION_COMPONENT - return ncurses_version; + return lib_version; } -#endif /* NCURSES_VERSION */ +#endif /* NCURSES_VERSION || PDCURSES */ /*[clinic input] @permit_long_summary @@ -9110,25 +9284,25 @@ cursesmodule_exec(PyObject *module) return -1; } -#ifdef NCURSES_VERSION - /* ncurses_version */ +#if defined(NCURSES_VERSION) || defined(PDCURSES) + /* ncurses_version or pdcurses_version */ PyTypeObject *version_type; - version_type = _PyStructSequence_NewType(&ncurses_version_desc, + version_type = _PyStructSequence_NewType(&curses_lib_version_desc, Py_TPFLAGS_DISALLOW_INSTANTIATION); if (version_type == NULL) { return -1; } - PyObject *ncurses_version = make_ncurses_version(version_type); + PyObject *lib_version = make_curses_lib_version(version_type); Py_DECREF(version_type); - if (ncurses_version == NULL) { + if (lib_version == NULL) { return -1; } - rc = PyDict_SetItemString(module_dict, "ncurses_version", ncurses_version); - Py_CLEAR(ncurses_version); + rc = PyDict_SetItemString(module_dict, CURSES_LIB_VERSION_ATTR, lib_version); + Py_CLEAR(lib_version); if (rc < 0) { return -1; } -#endif /* NCURSES_VERSION */ +#endif /* NCURSES_VERSION || PDCURSES */ #define SetDictInt(NAME, VALUE) \ do { \ @@ -9255,7 +9429,7 @@ cursesmodule_exec(PyObject *module) SetDictInt("COLOR_CYAN", COLOR_CYAN); SetDictInt("COLOR_WHITE", COLOR_WHITE); -#if defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES) +#ifdef HAVE_CURSES_GETMOUSE /* Mouse-related constants */ SetDictInt("BUTTON1_PRESSED", BUTTON1_PRESSED); SetDictInt("BUTTON1_RELEASED", BUTTON1_RELEASED); diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h index dfd589ba45089e..535577f2695591 100644 --- a/Modules/clinic/_cursesmodule.c.h +++ b/Modules/clinic/_cursesmodule.c.h @@ -1068,7 +1068,7 @@ _curses_window_echochar(PyObject *self, PyObject *args) return return_value; } -#if (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) +#if defined(HAVE_CURSES_GETMOUSE) PyDoc_STRVAR(_curses_window_enclose__doc__, "enclose($self, y, x, /)\n" @@ -1111,9 +1111,9 @@ _curses_window_enclose(PyObject *self, PyObject *const *args, Py_ssize_t nargs) return return_value; } -#endif /* (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) */ +#endif /* defined(HAVE_CURSES_GETMOUSE) */ -#if (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) +#if defined(HAVE_CURSES_GETMOUSE) PyDoc_STRVAR(_curses_window_mouse_trafo__doc__, "mouse_trafo($self, y, x, to_screen, /)\n" @@ -1168,7 +1168,7 @@ _curses_window_mouse_trafo(PyObject *self, PyObject *const *args, Py_ssize_t nar return return_value; } -#endif /* (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) */ +#endif /* defined(HAVE_CURSES_GETMOUSE) */ PyDoc_STRVAR(_curses_window_getbkgd__doc__, "getbkgd($self, /)\n" @@ -2493,7 +2493,7 @@ _curses_cbreak(PyObject *module, PyObject *const *args, Py_ssize_t nargs) return return_value; } -#if ((defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20240427) || defined(PDCURSES)) +#if defined(HAVE_CURSES_IS_CBREAK) PyDoc_STRVAR(_curses_is_cbreak__doc__, "is_cbreak($module, /)\n" @@ -2513,9 +2513,9 @@ _curses_is_cbreak(PyObject *module, PyObject *Py_UNUSED(ignored)) return _curses_is_cbreak_impl(module); } -#endif /* ((defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20240427) || defined(PDCURSES)) */ +#endif /* defined(HAVE_CURSES_IS_CBREAK) */ -#if ((defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20240427) || defined(PDCURSES)) +#if defined(HAVE_CURSES_IS_CBREAK) PyDoc_STRVAR(_curses_is_echo__doc__, "is_echo($module, /)\n" @@ -2535,9 +2535,9 @@ _curses_is_echo(PyObject *module, PyObject *Py_UNUSED(ignored)) return _curses_is_echo_impl(module); } -#endif /* ((defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20240427) || defined(PDCURSES)) */ +#endif /* defined(HAVE_CURSES_IS_CBREAK) */ -#if ((defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20240427) || defined(PDCURSES)) +#if defined(HAVE_CURSES_IS_CBREAK) PyDoc_STRVAR(_curses_is_nl__doc__, "is_nl($module, /)\n" @@ -2557,9 +2557,9 @@ _curses_is_nl(PyObject *module, PyObject *Py_UNUSED(ignored)) return _curses_is_nl_impl(module); } -#endif /* ((defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20240427) || defined(PDCURSES)) */ +#endif /* defined(HAVE_CURSES_IS_CBREAK) */ -#if ((defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20240427) || defined(PDCURSES)) +#if defined(HAVE_CURSES_IS_CBREAK) PyDoc_STRVAR(_curses_is_raw__doc__, "is_raw($module, /)\n" @@ -2579,7 +2579,7 @@ _curses_is_raw(PyObject *module, PyObject *Py_UNUSED(ignored)) return _curses_is_raw_impl(module); } -#endif /* ((defined(NCURSES_EXT_FUNCS) && NCURSES_EXT_FUNCS >= 20240427) || defined(PDCURSES)) */ +#endif /* defined(HAVE_CURSES_IS_CBREAK) */ PyDoc_STRVAR(_curses_color_content__doc__, "color_content($module, color_number, /)\n" @@ -2941,7 +2941,7 @@ _curses_getsyx(PyObject *module, PyObject *Py_UNUSED(ignored)) #endif /* defined(getsyx) */ -#if (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) +#if defined(HAVE_CURSES_GETMOUSE) PyDoc_STRVAR(_curses_getmouse__doc__, "getmouse($module, /)\n" @@ -2964,9 +2964,9 @@ _curses_getmouse(PyObject *module, PyObject *Py_UNUSED(ignored)) return _curses_getmouse_impl(module); } -#endif /* (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) */ +#endif /* defined(HAVE_CURSES_GETMOUSE) */ -#if (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) +#if defined(HAVE_CURSES_GETMOUSE) PyDoc_STRVAR(_curses_ungetmouse__doc__, "ungetmouse($module, id, x, y, z, bstate, /)\n" @@ -3053,7 +3053,7 @@ _curses_ungetmouse(PyObject *module, PyObject *const *args, Py_ssize_t nargs) return return_value; } -#endif /* (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) */ +#endif /* defined(HAVE_CURSES_GETMOUSE) */ PyDoc_STRVAR(_curses_getwin__doc__, "getwin($module, file, /)\n" @@ -4252,7 +4252,7 @@ _curses_meta(PyObject *module, PyObject *arg) return return_value; } -#if (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) && (defined(HAVE_CURSES_HAS_MOUSE) || defined(PDCURSES)) +#if defined(HAVE_CURSES_GETMOUSE) && defined(HAVE_CURSES_HAS_MOUSE) PyDoc_STRVAR(_curses_has_mouse__doc__, "has_mouse($module, /)\n" @@ -4272,9 +4272,9 @@ _curses_has_mouse(PyObject *module, PyObject *Py_UNUSED(ignored)) return _curses_has_mouse_impl(module); } -#endif /* (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) && (defined(HAVE_CURSES_HAS_MOUSE) || defined(PDCURSES)) */ +#endif /* defined(HAVE_CURSES_GETMOUSE) && defined(HAVE_CURSES_HAS_MOUSE) */ -#if (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) +#if defined(HAVE_CURSES_GETMOUSE) PyDoc_STRVAR(_curses_mouseinterval__doc__, "mouseinterval($module, interval, /)\n" @@ -4311,9 +4311,9 @@ _curses_mouseinterval(PyObject *module, PyObject *arg) return return_value; } -#endif /* (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) */ +#endif /* defined(HAVE_CURSES_GETMOUSE) */ -#if (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) +#if defined(HAVE_CURSES_GETMOUSE) PyDoc_STRVAR(_curses_mousemask__doc__, "mousemask($module, newmask, /)\n" @@ -4364,7 +4364,7 @@ _curses_mousemask(PyObject *module, PyObject *arg) return return_value; } -#endif /* (defined(HAVE_CURSES_GETMOUSE) || defined(PDCURSES)) */ +#endif /* defined(HAVE_CURSES_GETMOUSE) */ PyDoc_STRVAR(_curses_napms__doc__, "napms($module, ms, /)\n" @@ -5745,7 +5745,7 @@ _curses_slk_attrset(PyObject *module, PyObject *arg) return return_value; } -#if (defined(NCURSES_EXT_FUNCS) || defined(PDCURSES)) +#if defined(HAVE_CURSES_SLK_ATTR) PyDoc_STRVAR(_curses_slk_attr__doc__, "slk_attr($module, /)\n" @@ -5765,7 +5765,7 @@ _curses_slk_attr(PyObject *module, PyObject *Py_UNUSED(ignored)) return _curses_slk_attr_impl(module); } -#endif /* (defined(NCURSES_EXT_FUNCS) || defined(PDCURSES)) */ +#endif /* defined(HAVE_CURSES_SLK_ATTR) */ #if defined(HAVE_CURSES_SLK_ATTR_ON) @@ -6234,4 +6234,4 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #ifndef _CURSES_ASSUME_DEFAULT_COLORS_METHODDEF #define _CURSES_ASSUME_DEFAULT_COLORS_METHODDEF #endif /* !defined(_CURSES_ASSUME_DEFAULT_COLORS_METHODDEF) */ -/*[clinic end generated code: output=cb5525c88ae5c440 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=e2add67dd6eecef3 input=a9049054013a1b77]*/ diff --git a/PC/pdcurses/term.h b/PC/pdcurses/term.h new file mode 100644 index 00000000000000..7cb92646b325af --- /dev/null +++ b/PC/pdcurses/term.h @@ -0,0 +1,43 @@ +/* Minimal for building the curses module against PDCurses. + + PDCurses does not implement terminfo, but _cursesmodule.c references a + handful of terminfo functions (setupterm(), tigetstr(), tparm(), ...) + unconditionally. PDCurses ships a of its own, declaring the same + stubs it exports from the library; the module cannot use it, because the + declarations are marked for import from the DLL and the module defines its + own -- setupterm() has to report success, where PDCurses reports an error. + Anyone who calls the others gets an error, which is the best that can be + done without a terminfo database. */ + +#ifndef PY_PDCURSES_TERM_H +#define PY_PDCURSES_TERM_H 1 + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + const char *_termname; +} TERMINAL; + +extern TERMINAL *cur_term; + +int del_curterm(TERMINAL *); +int putp(const char *); +int restartterm(const char *, int, int *); +TERMINAL *set_curterm(TERMINAL *); +int setupterm(const char *, int, int *); +int tigetflag(const char *); +int tigetnum(const char *); +char *tigetstr(const char *); +char *tparm(const char *, long, long, long, long, long, + long, long, long, long); +int tputs(const char *, int, int (*)(int)); + +#ifdef __cplusplus +} +#endif + +#endif /* !PY_PDCURSES_TERM_H */ diff --git a/PC/pdcurses/terminfo.c b/PC/pdcurses/terminfo.c new file mode 100644 index 00000000000000..331e337bdfd087 --- /dev/null +++ b/PC/pdcurses/terminfo.c @@ -0,0 +1,94 @@ +/* Terminfo stubs for building the curses module against PDCurses. + + PDCurses has no terminfo database, so these functions cannot do anything + useful. It exports stubs of its own, but they are declared for import from + the DLL and its setupterm() reports an error, which the module needs to + succeed; these definitions replace them. Each of the others reports failure + using the value that terminfo assigns to a missing capability. */ + +#include +#include "term.h" + +TERMINAL *cur_term = NULL; + +int +setupterm(const char *term, int fildes, int *errret) +{ + /* PDCurses has no terminfo database and initscr() works without one, so + report success: this is the no-op that curses.initscr() expects before + it calls the real initscr(). The capability queries below still report + "not found", which is the truthful answer with no terminfo. */ + if (errret != NULL) { + *errret = 1; /* 1: terminal is hardcopy/normal, OK */ + } + return OK; +} + +int +del_curterm(TERMINAL *oterm) +{ + return ERR; +} + +TERMINAL * +set_curterm(TERMINAL *nterm) +{ + return NULL; +} + +int +restartterm(const char *term, int fildes, int *errret) +{ + if (errret != NULL) { + *errret = 0; + } + return ERR; +} + +int +tigetflag(const char *capname) +{ + return -1; /* -1: capability is not a boolean */ +} + +int +tigetnum(const char *capname) +{ + return -2; /* -2: capability is not numeric */ +} + +char * +tigetstr(const char *capname) +{ + return (char *)-1; /* (char *)-1: capability is not a string */ +} + +char * +tparm(const char *str, long p1, long p2, long p3, long p4, long p5, + long p6, long p7, long p8, long p9) +{ + return NULL; +} + +int +tputs(const char *str, int affcnt, int (*outc)(int)) +{ + /* No terminfo padding information is available, so emit the string as-is + through the caller's output function. This is enough for putp() to + write a literal (non-capability) string. */ + if (str == NULL || str == (const char *)-1) { + return ERR; + } + while (*str != '\0') { + if (outc((unsigned char)*str++) == EOF) { + return ERR; + } + } + return OK; +} + +int +putp(const char *str) +{ + return tputs(str, 1, putchar); +} diff --git a/PCbuild/_curses.vcxproj b/PCbuild/_curses.vcxproj new file mode 100644 index 00000000000000..631c5fd508ef4f --- /dev/null +++ b/PCbuild/_curses.vcxproj @@ -0,0 +1,126 @@ + + + + + Debug + ARM + + + Debug + ARM64 + + + Debug + Win32 + + + Debug + x64 + + + PGInstrument + ARM + + + PGInstrument + ARM64 + + + PGInstrument + Win32 + + + PGInstrument + x64 + + + PGUpdate + ARM + + + PGUpdate + ARM64 + + + PGUpdate + Win32 + + + PGUpdate + x64 + + + Release + ARM + + + Release + ARM64 + + + Release + Win32 + + + Release + x64 + + + + {C4B8E1F2-3D6A-4B5C-9E7F-1A2B3C4D5E61} + _curses + Win32Proj + + + + + DynamicLibrary + NotSet + + + + $(PyStdlibPydExt) + + + + + + + + + + <_ProjectFileVersion>10.0.30319.1 + + + + ..\PC\pdcurses;$(PDCursesDir);$(PDCursesDir)\common;%(AdditionalIncludeDirectories) + HAVE_CURSES_H;PDC_DLL_BUILD;PDC_WIDE;%(PreprocessorDefinitions) + + + user32.lib;advapi32.lib;gdi32.lib;comdlg32.lib;shell32.lib;%(AdditionalDependencies) + + + + + + + + + + + + + + + {cf7ac3d1-e2df-41d2-bea6-1e2556cdea26} + false + + + {c4b8e1f2-3d6a-4b5c-9e7f-1a2b3c4d5e60} + false + + + + + + diff --git a/PCbuild/_curses_panel.vcxproj b/PCbuild/_curses_panel.vcxproj new file mode 100644 index 00000000000000..f98da136c92795 --- /dev/null +++ b/PCbuild/_curses_panel.vcxproj @@ -0,0 +1,122 @@ + + + + + Debug + ARM + + + Debug + ARM64 + + + Debug + Win32 + + + Debug + x64 + + + PGInstrument + ARM + + + PGInstrument + ARM64 + + + PGInstrument + Win32 + + + PGInstrument + x64 + + + PGUpdate + ARM + + + PGUpdate + ARM64 + + + PGUpdate + Win32 + + + PGUpdate + x64 + + + Release + ARM + + + Release + ARM64 + + + Release + Win32 + + + Release + x64 + + + + {C4B8E1F2-3D6A-4B5C-9E7F-1A2B3C4D5E62} + _curses_panel + Win32Proj + + + + + DynamicLibrary + NotSet + + + + $(PyStdlibPydExt) + + + + + + + + + + <_ProjectFileVersion>10.0.30319.1 + + + + ..\PC\pdcurses;$(PDCursesDir);$(PDCursesDir)\common;%(AdditionalIncludeDirectories) + HAVE_CURSES_H;HAVE_PANEL_H;PDC_DLL_BUILD;PDC_WIDE;%(PreprocessorDefinitions) + + + user32.lib;advapi32.lib;gdi32.lib;comdlg32.lib;shell32.lib;%(AdditionalDependencies) + + + + + + + + + + + {cf7ac3d1-e2df-41d2-bea6-1e2556cdea26} + false + + + {c4b8e1f2-3d6a-4b5c-9e7f-1a2b3c4d5e60} + false + + + + + + diff --git a/PCbuild/pcbuild.proj b/PCbuild/pcbuild.proj index 53aec0276beaee..eab15054b6c09a 100644 --- a/PCbuild/pcbuild.proj +++ b/PCbuild/pcbuild.proj @@ -13,6 +13,13 @@ true true true + + $(PDCURSES_DIR) + $(PDCursesDir)\ + true + false false @@ -74,6 +81,7 @@ + diff --git a/PCbuild/pdcurses.vcxproj b/PCbuild/pdcurses.vcxproj new file mode 100644 index 00000000000000..c59c299e2554ed --- /dev/null +++ b/PCbuild/pdcurses.vcxproj @@ -0,0 +1,121 @@ + + + + + Debug + ARM + + + Debug + ARM64 + + + Debug + Win32 + + + Debug + x64 + + + PGInstrument + ARM + + + PGInstrument + ARM64 + + + PGInstrument + Win32 + + + PGInstrument + x64 + + + PGUpdate + ARM + + + PGUpdate + ARM64 + + + PGUpdate + Win32 + + + PGUpdate + x64 + + + Release + ARM + + + Release + ARM64 + + + Release + Win32 + + + Release + x64 + + + + {C4B8E1F2-3D6A-4B5C-9E7F-1A2B3C4D5E60} + pdcurses + false + + + + + + DynamicLibrary + NotSet + + + + + + + + + + + <_ProjectFileVersion>10.0.30319.1 + pdcurses + + + + $(PDCursesDir);$(PDCursesDir)\common;%(AdditionalIncludeDirectories) + + PDC_DLL_BUILD;CURSES_LIBRARY;PDC_WIDE;NDEBUG;%(PreprocessorDefinitions) + Level1 + %(AdditionalOptions) -Wno-unused + + + + user32.lib;advapi32.lib;gdi32.lib;comdlg32.lib;shell32.lib;winmm.lib;%(AdditionalDependencies) + + + + + + + + + + diff --git a/PCbuild/python.props b/PCbuild/python.props index 8d931bba28a389..b7cfaaaa1d54e7 100644 --- a/PCbuild/python.props +++ b/PCbuild/python.props @@ -112,6 +112,11 @@ $(ExternalsDir)\zlib-1.3.1\ $(ExternalsDir)\zlib-ng-2.2.4\ $(ExternalsDir)\zstd-1.5.7\ + + $(PDCURSES_DIR) + $(PDCursesDir)\ @@ -128,6 +133,10 @@ true + + + true + false