Skip to content

Commit 19c9730

Browse files
Merge branch 'main' into feature/command-option
2 parents 4fb7b2d + a12e801 commit 19c9730

File tree

10 files changed

+133
-8
lines changed

10 files changed

+133
-8
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
repos:
22
- repo: https://github.com/astral-sh/ruff-pre-commit
33
# Ruff version.
4-
rev: v0.11.7
4+
rev: v0.14.10
55
hooks:
66
# Run the linter.
77
- id: ruff

AUTHORS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ Contributors:
145145
* fbdb
146146
* Charbel Jacquin (charbeljc)
147147
* Diego
148+
* Devadathan M B (devadathanmb)
149+
* Charalampos Stratakis
148150

149151
Creator:
150152
--------

changelog.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
Upcoming (TBD)
22
==============
33

4+
Features:
5+
---------
6+
* Add support for `\\T` prompt escape sequence to display transaction status (similar to psql's `%x`).
7+
* Add cursor shape support for vi mode. When ``vi = True``, the terminal cursor now
8+
reflects the current editing mode: beam in INSERT, block in NORMAL, underline in REPLACE.
9+
Uses prompt_toolkit's ``ModalCursorShapeConfig``.
10+
11+
4.4.0 (2025-12-24)
12+
==================
13+
414
Features:
515
---------
616
* Add support for `init-command` to run when the connection is established.

pgcli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "4.3.0"
1+
__version__ = "4.4.0"

pgcli/main.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
)
4949
from prompt_toolkit.history import FileHistory
5050
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
51+
from prompt_toolkit.cursor_shapes import ModalCursorShapeConfig
5152
from pygments.lexers.sql import PostgresLexer
5253

5354
from pgspecial.main import PGSpecial, NO_QUERY, PAGER_OFF, PAGER_LONG_OUTPUT
@@ -208,6 +209,7 @@ def __init__(
208209
self.output_file = None
209210
self.pgspecial = PGSpecial()
210211

212+
self.hide_named_query_text = "hide_named_query_text" in c["main"] and c["main"].as_bool("hide_named_query_text")
211213
self.explain_mode = False
212214
self.multi_line = c["main"].as_bool("multi_line")
213215
self.multiline_mode = c["main"].get("multi_line_mode", "psql")
@@ -307,7 +309,28 @@ def __init__(
307309
def quit(self):
308310
raise PgCliQuitError
309311

312+
def toggle_named_query_quiet(self):
313+
"""Toggle hiding of named query text"""
314+
self.hide_named_query_text = not self.hide_named_query_text
315+
status = "ON" if self.hide_named_query_text else "OFF"
316+
message = f"Named query quiet mode: {status}"
317+
return [(None, None, None, message)]
318+
319+
def _is_named_query_execution(self, text):
320+
"""Check if the command is a named query execution (\n <name>)."""
321+
text = text.strip()
322+
return text.startswith("\\n ") and not text.startswith("\\ns ") and not text.startswith("\\nd ")
323+
310324
def register_special_commands(self):
325+
self.pgspecial.register(
326+
self.toggle_named_query_quiet,
327+
"\\nq",
328+
"\\nq",
329+
"Toggle named query quiet mode (hide query text)",
330+
arg_type=NO_QUERY,
331+
case_sensitive=True,
332+
)
333+
311334
self.pgspecial.register(
312335
self.change_db,
313336
"\\c",
@@ -828,7 +851,14 @@ def execute_command(self, text, handle_closed_connection=True):
828851
if self.output_file and not text.startswith(("\\o ", "\\log-file", "\\? ", "\\echo ")):
829852
try:
830853
with open(self.output_file, "a", encoding="utf-8") as f:
831-
click.echo(text, file=f)
854+
should_hide = (
855+
self.hide_named_query_text
856+
and query.is_special
857+
and query.successful
858+
and self._is_named_query_execution(text)
859+
)
860+
if not should_hide:
861+
click.echo(text, file=f)
832862
click.echo("\n".join(output), file=f)
833863
click.echo("", file=f) # extra newline
834864
except OSError as e:
@@ -842,7 +872,14 @@ def execute_command(self, text, handle_closed_connection=True):
842872
try:
843873
with open(self.log_file, "a", encoding="utf-8") as f:
844874
click.echo(dt.datetime.now().isoformat(), file=f) # timestamp log
845-
click.echo(text, file=f)
875+
should_hide = (
876+
self.hide_named_query_text
877+
and query.is_special
878+
and query.successful
879+
and self._is_named_query_execution(text)
880+
)
881+
if not should_hide:
882+
click.echo(text, file=f)
846883
click.echo("\n".join(output), file=f)
847884
click.echo("", file=f) # extra newline
848885
except OSError as e:
@@ -1070,6 +1107,7 @@ def get_continuation(width, line_number, is_soft_wrap):
10701107
enable_suspend=True,
10711108
editing_mode=EditingMode.VI if self.vi_mode else EditingMode.EMACS,
10721109
search_ignore_case=True,
1110+
cursor=ModalCursorShapeConfig(),
10731111
)
10741112

10751113
return prompt_app
@@ -1155,6 +1193,18 @@ def _evaluate_command(self, text):
11551193
style_output=self.style_output,
11561194
max_field_width=self.max_field_width,
11571195
)
1196+
1197+
# Hide query text for named queries in quiet mode
1198+
if (
1199+
self.hide_named_query_text
1200+
and is_special
1201+
and success
1202+
and self._is_named_query_execution(text)
1203+
and title
1204+
and title.startswith("> ")
1205+
):
1206+
title = None
1207+
11581208
execution = time() - start
11591209
formatted = format_output(title, cur, headers, status, settings, self.explain_mode)
11601210

@@ -1278,6 +1328,7 @@ def get_prompt(self, string):
12781328
string = string.replace("\\i", str(self.pgexecute.pid) or "(none)")
12791329
string = string.replace("\\#", "#" if self.pgexecute.superuser else ">")
12801330
string = string.replace("\\n", "\n")
1331+
string = string.replace("\\T", self.pgexecute.transaction_indicator)
12811332
return string
12821333

12831334
def get_last_query(self):

pgcli/packages/sqlcompletion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ def suggest_special(text):
270270
return (Schema(), Function(schema=None, usage="special"))
271271
return (Schema(), rel_type(schema=None))
272272

273-
if cmd in ["\\n", "\\ns", "\\nd"]:
273+
if cmd in ["\\n", "\\ns", "\\nd", "\\nq"]:
274274
return (NamedQuery(),)
275275

276276
return (Keyword(), Special())

pgcli/pgclirc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ search_path_filter = False
116116
# Timing of sql statements and table rendering.
117117
timing = True
118118

119+
# Hide the query text when executing named queries (\n <name>).
120+
# Only the query results will be displayed.
121+
# Can be toggled at runtime with \nq command.
122+
hide_named_query_text = False
123+
119124
# Show/hide the informational toolbar with function keymap at the footer.
120125
show_bottom_toolbar = True
121126

@@ -171,6 +176,7 @@ verbose_errors = False
171176
# \i - Postgres PID
172177
# \# - "@" sign if logged in as superuser, '>' in other case
173178
# \n - Newline
179+
# \T - Transaction status: '*' if in a valid transaction, '!' if in a failed transaction, '?' if disconnected, empty otherwise
174180
# \dsn_alias - name of dsn connection string alias if -D option is used (empty otherwise)
175181
# \x1b[...m - insert ANSI escape sequence
176182
# eg: prompt = '\x1b[35m\u@\x1b[32m\h:\x1b[36m\d>'

pgcli/pgexecute.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,19 @@ def valid_transaction(self):
298298
status = self.conn.info.transaction_status
299299
return status == psycopg.pq.TransactionStatus.ACTIVE or status == psycopg.pq.TransactionStatus.INTRANS
300300

301+
def is_connection_closed(self):
302+
return self.conn.info.transaction_status == psycopg.pq.TransactionStatus.UNKNOWN
303+
304+
@property
305+
def transaction_indicator(self):
306+
if self.is_connection_closed():
307+
return "?"
308+
if self.failed_transaction():
309+
return "!"
310+
if self.valid_transaction():
311+
return "*"
312+
return ""
313+
301314
def run(
302315
self,
303316
statement,
@@ -492,7 +505,8 @@ def view_definition(self, spec):
492505
else:
493506
template = "CREATE OR REPLACE VIEW {name} AS \n{stmt}"
494507
return (
495-
psycopg.sql.SQL(template)
508+
psycopg.sql
509+
.SQL(template)
496510
.format(
497511
name=psycopg.sql.Identifier(result.nspname, result.relname),
498512
stmt=psycopg.sql.SQL(result.viewdef),

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ description = "CLI for Postgres Database. With auto-completion and syntax highli
66
readme = "README.rst"
77
classifiers = [
88
"Intended Audience :: Developers",
9-
"License :: OSI Approved :: BSD License",
109
"Operating System :: Unix",
1110
"Programming Language :: Python",
1211
"Programming Language :: Python :: 3",
@@ -25,7 +24,8 @@ urls = { Homepage = "https://pgcli.com" }
2524
requires-python = ">=3.9"
2625
dependencies = [
2726
"pgspecial>=2.0.0",
28-
"click >= 4.1,<8.1.8",
27+
# Click 8.1.8 through 8.3.0 have broken pager invocation for multi-argument PAGER values, which causes behave test failures.
28+
"click >= 4.1, != 8.1.8, != 8.2.*, != 8.3.0, < 9",
2929
"Pygments>=2.0", # Pygments has to be Capitalcased.
3030
# We still need to use pt-2 unless pt-3 released on Fedora32
3131
# see: https://github.com/dbcli/pgcli/pull/1197
@@ -85,6 +85,7 @@ pgcli = ["pgclirc", "packages/pgliterals/pgliterals.json"]
8585
[tool.ruff]
8686
target-version = 'py39'
8787
line-length = 140
88+
show-fixes = true
8889

8990
[tool.ruff.lint]
9091
select = [

tests/test_main.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,47 @@ def test_duration_in_words(duration_in_seconds, words):
577577
assert duration_in_words(duration_in_seconds) == words
578578

579579

580+
@pytest.mark.parametrize(
581+
"transaction_indicator,expected",
582+
[
583+
("*", "*testuser"), # valid transaction
584+
("!", "!testuser"), # failed transaction
585+
("?", "?testuser"), # connection closed
586+
("", "testuser"), # idle
587+
],
588+
)
589+
def test_get_prompt_with_transaction_status(transaction_indicator, expected):
590+
cli = PGCli()
591+
cli.pgexecute = mock.MagicMock()
592+
cli.pgexecute.user = "testuser"
593+
cli.pgexecute.dbname = "testdb"
594+
cli.pgexecute.host = "localhost"
595+
cli.pgexecute.short_host = "localhost"
596+
cli.pgexecute.port = 5432
597+
cli.pgexecute.pid = 12345
598+
cli.pgexecute.superuser = False
599+
cli.pgexecute.transaction_indicator = transaction_indicator
600+
601+
result = cli.get_prompt("\\T\\u")
602+
assert result == expected
603+
604+
605+
def test_get_prompt_transaction_status_in_full_prompt():
606+
cli = PGCli()
607+
cli.pgexecute = mock.MagicMock()
608+
cli.pgexecute.user = "user"
609+
cli.pgexecute.dbname = "mydb"
610+
cli.pgexecute.host = "db.example.com"
611+
cli.pgexecute.short_host = "db.example.com"
612+
cli.pgexecute.port = 5432
613+
cli.pgexecute.pid = 12345
614+
cli.pgexecute.superuser = False
615+
cli.pgexecute.transaction_indicator = "*"
616+
617+
result = cli.get_prompt("\\T\\u@\\h:\\d> ")
618+
assert result == "*user@db.example.com:mydb> "
619+
620+
580621
@dbtest
581622
def test_notifications(executor):
582623
run(executor, "listen chan1")

0 commit comments

Comments
 (0)