Skip to content

Commit be7030b

Browse files
committed
ttktui: add experimental TermTk based text user interface
Launch in a terminal with command line argument `nicotine --tui` Depends on ceccopierangiolieugenio/pyTermTk#469
1 parent 8d15975 commit be7030b

File tree

12 files changed

+1272
-23
lines changed

12 files changed

+1272
-23
lines changed

pynicotine/__init__.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ def check_arguments():
6868
"-n", "--headless", action="store_true",
6969
help=_("start the program in headless mode (no GUI)")
7070
)
71+
parser.add_argument(
72+
"-t", "--tui", action="store_true",
73+
help=_("start the program in text terminal mode (TermTk TUI)")
74+
)
7175
parser.add_argument(
7276
"-v", "--version", action="version", version=f"{__application_name__} {__version__}",
7377
help=_("display version and exit")
@@ -94,7 +98,7 @@ def check_arguments():
9498
core.cli_interface_address = args.bindip
9599
core.cli_listen_port = args.port
96100

97-
return args.headless, args.hidden, args.ci_mode, args.isolated, args.rescan, multi_instance
101+
return args.headless, args.tui, args.hidden, args.ci_mode, args.isolated, args.rescan, multi_instance
98102

99103

100104
def check_python_version():
@@ -188,17 +192,28 @@ def run():
188192
set_up_python()
189193
rename_process(b"nicotine")
190194

191-
headless, hidden, ci_mode, isolated_mode, rescan, multi_instance = check_arguments()
195+
headless, tui, hidden, ci_mode, isolated_mode, rescan, multi_instance = check_arguments()
192196
error = check_python_version()
193197

194198
if error:
195199
print(error)
196200
return 1
197201

198-
core.init_components(
199-
enabled_components={"cli", "shares"} if rescan else None,
200-
isolated_mode=isolated_mode
201-
)
202+
if rescan:
203+
enabled_components = {"cli", "shares"}
204+
205+
elif tui:
206+
enabled_components = {
207+
"error_handler", "signal_handler", "portmapper", "network_thread", "shares", "users",
208+
"notifications", "network_filter", "now_playing", "statistics", "port_checker", "update_checker",
209+
"search", "downloads", "uploads", "interests", "userbrowse", "userinfo", "buddies",
210+
"chatrooms", "privatechat", "pluginhandler"
211+
}
212+
213+
else:
214+
enabled_components = None # Enable all components by default
215+
216+
core.init_components(enabled_components=enabled_components, isolated_mode=isolated_mode)
202217

203218
# Dump tracebacks for C modules (in addition to pure Python code)
204219
try:
@@ -214,8 +229,16 @@ def run():
214229
if rescan:
215230
return rescan_shares()
216231

217-
# Initialize GTK-based GUI
218-
if not headless:
232+
if tui:
233+
# Initialize TermTk-based TUI
234+
from pynicotine import ttktui as application
235+
exit_code = application.run(ci_mode)
236+
237+
if exit_code is not None:
238+
return exit_code
239+
240+
elif not headless:
241+
# Initialize GTK-based GUI
219242
from pynicotine import gtkgui as application
220243
exit_code = application.run(hidden, ci_mode, isolated_mode, multi_instance)
221244

pynicotine/cli.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
# SPDX-FileCopyrightText: 2022-2025 Nicotine+ Contributors
22
# SPDX-License-Identifier: GPL-3.0-or-later
33

4+
try:
5+
# Enable line editing and history
6+
import readline
7+
8+
try:
9+
readline.get_line_buffer() # pylint: disable=no-member
10+
except Exception as curses_error:
11+
raise ImportError from curses_error
12+
13+
READLINE_ENABLED = True
14+
15+
except ImportError:
16+
# Readline is not available on this OS
17+
READLINE_ENABLED = False
18+
419
import sys
520
import time
621

@@ -19,14 +34,7 @@ def __init__(self):
1934

2035
super().__init__(name="CLIInputProcessor", daemon=True)
2136

22-
try:
23-
# Enable line editing and history
24-
import readline # noqa: F401 # pylint:disable=unused-import
25-
26-
except ImportError:
27-
# Readline is not available on this OS
28-
pass
29-
37+
self.has_buffer = READLINE_ENABLED
3038
self.has_custom_prompt = False
3139
self.prompt_message = ""
3240
self.prompt_callback = None
@@ -43,7 +51,9 @@ def run(self):
4351

4452
except Exception as error:
4553
log.add_debug("CLI input prompt is no longer available: %s", error)
46-
return
54+
break
55+
56+
self.has_buffer = False
4757

4858
def _handle_prompt_callback(self, user_input, callback):
4959

@@ -89,6 +99,13 @@ def _handle_prompt(self):
8999
# No custom prompt, treat input as command
90100
self._handle_prompt_command(user_input)
91101

102+
def get_prompt_line(self):
103+
104+
if not self.has_buffer:
105+
return ""
106+
107+
return f"{self.prompt_message}{readline.get_line_buffer()}" # pylint: disable=no-member
108+
92109

93110
class CLI:
94111
__slots__ = ("_input_processor", "_log_message_queue", "_tty_attributes")
@@ -127,10 +144,10 @@ def prompt(self, message, callback, is_silent=False):
127144
self._input_processor.prompt_callback = callback
128145
self._input_processor.prompt_silent = is_silent
129146

130-
def _print_log_message(self, log_message):
147+
def _print_log_message(self, log_message, prompt_line=None):
131148

132149
try:
133-
print(log_message, flush=True)
150+
print(f"\x1b[2K{log_message}\n{prompt_line}", end="", flush=True)
134151

135152
except OSError:
136153
# stdout is gone, prevent future errors
@@ -149,12 +166,13 @@ def _log_message(self, timestamp_format, msg, _title, _level):
149166
else:
150167
log_message = msg
151168

152-
if self._input_processor.has_custom_prompt:
153-
# Don't print log messages while custom prompt is active
169+
if self._input_processor.has_custom_prompt and not READLINE_ENABLED:
170+
# Unless there's a way to avoid overwriting the prompt and user
171+
# input, don't print log messages while custom prompt is active
154172
self._log_message_queue.append(log_message)
155173
return
156174

157-
self._print_log_message(log_message)
175+
self._print_log_message(log_message, prompt_line=self._input_processor.get_prompt_line())
158176

159177
def _quit(self):
160178
"""Restores TTY attributes and re-enables echo on quit."""

pynicotine/ttktui/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# SPDX-FileCopyrightText: 2025 Nicotine+ Contributors
2+
# SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
import os
5+
import sys
6+
7+
8+
def run(ci_mode):
9+
"""Run Nicotine+ TTk Text User Interface (TUI)."""
10+
11+
sys.path.append(os.path.join(sys.path[0], 'pynicotine', 'external', 'pyTermTk')) # ../libs/pyTermTk
12+
13+
from pynicotine.ttktui.application import Application
14+
return Application(ci_mode).run()

0 commit comments

Comments
 (0)