gitnc 0.1.3__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {gitnc-0.1.3 → gitnc-0.1.4}/CLAUDE.md +5 -1
  2. {gitnc-0.1.3 → gitnc-0.1.4}/PKG-INFO +1 -1
  3. {gitnc-0.1.3 → gitnc-0.1.4}/README.md +7 -0
  4. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/app.py +91 -9
  5. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/atom-one-dark.yaml +5 -0
  6. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/atom-one-light.yaml +5 -0
  7. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/catppuccin-frappe.yaml +5 -0
  8. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/catppuccin-latte.yaml +5 -0
  9. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/catppuccin-macchiato.yaml +5 -0
  10. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/catppuccin-mocha.yaml +5 -0
  11. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/dracula.yaml +5 -0
  12. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/flexoki.yaml +5 -0
  13. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/gruvbox.yaml +5 -0
  14. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/midnight-commander.yaml +7 -0
  15. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/monokai.yaml +5 -0
  16. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/nord.yaml +5 -0
  17. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/rose-pine-dawn.yaml +5 -0
  18. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/rose-pine-moon.yaml +5 -0
  19. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/rose-pine.yaml +5 -0
  20. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/solarized-dark.yaml +5 -0
  21. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/solarized-light.yaml +5 -0
  22. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/textual-ansi.yaml +5 -0
  23. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/textual-dark.yaml +5 -0
  24. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/textual-light.yaml +5 -0
  25. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/tokyo-night.yaml +5 -0
  26. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/ui_theme.py +8 -0
  27. {gitnc-0.1.3 → gitnc-0.1.4}/pyproject.toml +2 -1
  28. {gitnc-0.1.3 → gitnc-0.1.4}/tests/test_app.py +146 -0
  29. {gitnc-0.1.3 → gitnc-0.1.4}/.devcontainer/Dockerfile +0 -0
  30. {gitnc-0.1.3 → gitnc-0.1.4}/.devcontainer/devcontainer.json +0 -0
  31. {gitnc-0.1.3 → gitnc-0.1.4}/.gitignore +0 -0
  32. {gitnc-0.1.3 → gitnc-0.1.4}/LICENSE +0 -0
  33. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/__init__.py +0 -0
  34. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/__main__.py +0 -0
  35. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/git.py +0 -0
  36. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/terminal.py +0 -0
  37. {gitnc-0.1.3 → gitnc-0.1.4}/gitnc/themes/__init__.py +0 -0
  38. {gitnc-0.1.3 → gitnc-0.1.4}/tests/__init__.py +0 -0
  39. {gitnc-0.1.3 → gitnc-0.1.4}/tests/gitprocess.py +0 -0
  40. {gitnc-0.1.3 → gitnc-0.1.4}/tests/test_git.py +0 -0
  41. {gitnc-0.1.3 → gitnc-0.1.4}/tests/test_terminal.py +0 -0
  42. {gitnc-0.1.3 → gitnc-0.1.4}/tests/test_themes.py +0 -0
@@ -56,7 +56,11 @@ Menu chrome is composed once and stays mounted, so `action_set_theme` / `_load_s
56
56
 
57
57
  **Text selection colors:** selected text in the commit message box (Shift+arrows in the `TextArea`) is painted from `UITheme.text_selection_background` / `text_selection_foreground`, which `css_variables()` emits as Textual's own `$input-selection-background` / `$input-selection-foreground` — the variables `TextArea` and `Input` already read, so there is no selection CSS in the app. Unlike the chrome fields these are optional: an empty value emits nothing and leaves Textual's derived default (`$primary` lightened, at 40% over the background), because that default is fine in most themes and can't be written as a `$ref` `_resolve_refs()` understands. It is not fine in midnight-commander, where primary and background are both blue and a selection all but vanished; that theme sets `$warning` / `$background`, mcedit's dark-blue-on-cyan marked block. Set the selection through these fields, not through `variables`: `get_theme()` lets a `variables` entry win, so a second copy there would silently override the field.
58
58
 
59
- **Committing:** `action_commit_files()` asks twice, and the two questions are different. The first — only when a target still has unstaged changes — is `Stage N file(s) for commit?`, listing the `git add` commands that would run. The second always comes, after the message dialog: `_confirm_commit()` shows `Commit N file(s)?` over the commands from `GIT.commit_with_selection_commands()`, which is the last point at which the files going into the commit are visible before `commit_with_selection` walks the submodules and commits. The command list comes from that method rather than being rebuilt in `app.py` so the popup and the executor can't drift apart, and the message inside it is cut to `_commit_message_subject()` — its first line, marked with `TRUNCATION_MARKER` when there is more — because the popup renders one command per row and a multi-line `-m` argument would break that. N on it doesn't cancel: `_show_confirm()`'s `on_cancel` reopens the message editor — `CommitDialog`, or the external editor the settings name — on the message just written, so backing out of the confirmation costs nothing. That and the startup submodule question (see *Starting inside a submodule*), whose N still has to start the app, are the only callers of `on_cancel`; every other caller wants the default, which hands focus back to the active view.
59
+ **Warning notification color:** a `notify(..., severity="warning")` toast — today only the up-front branch-prefix warning (see *Committing*) — takes its text and left bar from `UITheme.warning_notification`, emitted as `$warning-notification` and applied by the `Toast.-warning` rules in the app `CSS`. It is a field of its own for the same reason the chrome colors are: Textual paints that bar in `$warning`, which is midnight-commander's cyan menu bar color and doesn't read as a warning there, so that theme sets a red (`#FF5555`) while every other theme keeps the default `$warning`.
60
+
61
+ **Committing:** `action_commit_files()` asks twice, and the two questions are different. The first — only when a target still has unstaged changes — is `Stage N file(s) for commit?`, listing the `git add` commands that would run. The second always comes, after the message dialog: `_confirm_commit()` shows `Commit N file(s)?` over the commands from `GIT.commit_with_selection_commands()`, which is the last point at which the files going into the commit are visible before `commit_with_selection` walks the submodules and commits. The command list comes from that method rather than being rebuilt in `app.py` so the popup and the executor can't drift apart, and the message inside it is cut to `_commit_message_subject()` — its first line, marked with `TRUNCATION_MARKER` when there is more — because the popup renders one command per row and a multi-line `-m` argument would break that. N on it doesn't cancel: `_show_confirm()`'s `on_cancel` reopens the message editor — `CommitDialog`, or the external editor the settings name — on the message just written, so backing out of the confirmation costs nothing. That, the prefix warning below, which reopens the message the same way, and the startup submodule question (see *Starting inside a submodule*), whose N still has to start the app, are the only callers of `on_cancel`; every other caller wants the default, which hands focus back to the active view.
62
+
63
+ A message prefixed with a branch the settings warn against gets a question of its own first: `_commit_result()` goes through `_confirm_commit_prefix()`, which — when the message starts with `<name>:` for a name in the `commit_prefix_warn_branches` setting — asks `Warning: commit message is prefixed with branch '…'. Continue?` over the message's subject line, and only on Y moves on to `Commit N file(s)?`. N reopens the message, the same way back the commit confirmation offers. It is a separate dialog rather than a row under the commands because a row only has to be overlooked. The setting is the *Warn on prefix* text row under the branch-prefix checkbox, a space- or comma-separated list read by `_branch_names()`. The check is on the message rather than the checked-out branch, because the prefill is editable and the prefix can have been typed by hand, and it comes after the editor because that is where the message is final whichever editor wrote it. `_begin_commit_message()` raises the same text — both come from `_commit_prefix_warning()` — as a warning notification when the prefill it is about to open already carries a listed prefix, so the user hears about it before writing the message around it.
60
64
 
61
65
  Cancelling the message dialog rolls the index back to where the flow found it. Both halves of the mutation are undone: `_restore_externals()` re-stages the files it unstaged, and `_unstage_pending()` unstages what the stage confirmation added, from the `_pending_commit_staged` list `_begin_commit_message()` was handed. Without the second half the files stayed staged, so a second `C` on the same selection found nothing left to stage and skipped straight past `Stage N file(s) for commit?` — the question never came back. Only entries that had *nothing* staged before go on that list (`not code.has_staged_changes`): an entry that was already partly staged had its index state built by the user, and `git restore --staged` would throw that away instead of putting it back. `_commit_submit()` clears the list without running it — the files it staged are the files it just committed.
62
66
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gitnc
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: GitNightCommander — a TUI git log browser built with Textual
5
5
  Project-URL: Homepage, https://github.com/thomashyldgaard/GitNightCommander
6
6
  Project-URL: Source, https://github.com/thomashyldgaard/GitNightCommander
@@ -92,6 +92,7 @@ first use, so no manual venv activation is needed for the commands below.
92
92
 
93
93
  ```sh
94
94
  hatch build # sdist + wheel into dist/
95
+ hatch run rebuild # same, removing the old artifacts from dist/ first
95
96
  ```
96
97
 
97
98
  The artifacts land in `dist/` as `gitnc-<version>-py3-none-any.whl` and
@@ -272,6 +273,12 @@ Options -> Settings configures the things it uses:
272
273
  - **Use branch name as prefix in commit messages** — prefills the message with
273
274
  `<branch>: `, and puts that prefix in front of a drafted message that does not
274
275
  already start with the branch name.
276
+ - **Warn on prefix** — branch names, separated by spaces or commas, that should
277
+ not end up in front of a commit message (e.g. `master main`). A message that
278
+ starts with `<name>:` is asked about in a confirmation of its own before the
279
+ `Commit N file(s)?` one: `Y` continues, `N` puts you back in the message to
280
+ change it. When the branch prefix setting prefills such a name, the same
281
+ warning also pops up as the message editor opens.
275
282
  - **Commit message editor** — where the message is written:
276
283
  - **Built-in editor** (the default) is the commit dialog described above.
277
284
  - **$EDITOR** runs the editor named by the `EDITOR` environment variable;
@@ -10,7 +10,7 @@ import subprocess
10
10
  import tempfile
11
11
  import textwrap
12
12
  import threading
13
- from collections.abc import Callable, Mapping
13
+ from collections.abc import Callable, Iterable, Mapping
14
14
  from contextlib import ExitStack
15
15
  from dataclasses import dataclass
16
16
  from pathlib import Path
@@ -815,6 +815,22 @@ def _caret_markup(text: str, *, column: int, style: str) -> str:
815
815
  return f"{_escape_markup(head)}[{style}]{_escape_markup(cell)}[/{style}]{_escape_markup(tail)}"
816
816
 
817
817
 
818
+ def _branch_names(text: str) -> list[str]:
819
+ """The branch names in a settings value that lists several, separated by
820
+ spaces or commas — neither of which a branch name can contain."""
821
+ return [name for name in re.split(r"[\s,]+", text) if name]
822
+
823
+
824
+ def _warned_commit_prefix(message: str, branches: Iterable[str]) -> str:
825
+ """Whichever of `branches` the message is prefixed with, or "".
826
+
827
+ The message is what is checked, not the branch that is checked out: the
828
+ prefill is editable, so the prefix can have been removed since — or typed
829
+ in by hand with the prefix setting off.
830
+ """
831
+ return next((branch for branch in branches if message.startswith(f"{branch}:")), "")
832
+
833
+
818
834
  def _setting_str(settings: Mapping[str, object], key: str, default: str) -> str:
819
835
  """`settings[key]` when it holds a string, else `default`.
820
836
 
@@ -897,6 +913,7 @@ class SettingsValues(NamedTuple):
897
913
  parse_suggestions: bool = False
898
914
  editor: str = COMMIT_EDITOR_BUILTIN
899
915
  editor_command: str = ""
916
+ prefix_warn_branches: str = ""
900
917
 
901
918
 
902
919
  # Rows of the settings dialog, in the order they are drawn. The checkbox rows
@@ -906,14 +923,15 @@ class SettingsValues(NamedTuple):
906
923
  # Experimental heading.
907
924
  SETTINGS_ROW_BRANCH = 0
908
925
  SETTINGS_ROW_PREFIX = 1
909
- SETTINGS_ROW_EDITOR_BUILTIN = 2
910
- SETTINGS_ROW_EDITOR_ENVIRONMENT = 3
911
- SETTINGS_ROW_EDITOR_COMMAND = 4
912
- SETTINGS_ROW_EDITOR_COMMAND_LINE = 5
913
- SETTINGS_ROW_COMMAND = 6
914
- SETTINGS_ROW_PROMPT = 7
915
- SETTINGS_ROW_PARSE = 8
916
- SETTINGS_ROW_COUNT = 9
926
+ SETTINGS_ROW_PREFIX_WARN = 2
927
+ SETTINGS_ROW_EDITOR_BUILTIN = 3
928
+ SETTINGS_ROW_EDITOR_ENVIRONMENT = 4
929
+ SETTINGS_ROW_EDITOR_COMMAND = 5
930
+ SETTINGS_ROW_EDITOR_COMMAND_LINE = 6
931
+ SETTINGS_ROW_COMMAND = 7
932
+ SETTINGS_ROW_PROMPT = 8
933
+ SETTINGS_ROW_PARSE = 9
934
+ SETTINGS_ROW_COUNT = 10
917
935
 
918
936
  # Which editor each radio row selects.
919
937
  SETTINGS_EDITOR_ROWS: dict[int, str] = {
@@ -1284,6 +1302,7 @@ class SettingsDialog(ModalScreen["SettingsValues | None"]):
1284
1302
  # rows differ only by their label and the value they carry.
1285
1303
  TEXT_ROWS: ClassVar[dict[int, str]] = {
1286
1304
  SETTINGS_ROW_BRANCH: "_default_branch",
1305
+ SETTINGS_ROW_PREFIX_WARN: "_prefix_warn_branches",
1287
1306
  SETTINGS_ROW_EDITOR_COMMAND_LINE: "_editor_command",
1288
1307
  SETTINGS_ROW_COMMAND: "_draft_command",
1289
1308
  SETTINGS_ROW_PROMPT: "_draft_prompt",
@@ -1303,11 +1322,13 @@ class SettingsDialog(ModalScreen["SettingsValues | None"]):
1303
1322
  parse_suggestions: bool = False,
1304
1323
  editor: str = COMMIT_EDITOR_BUILTIN,
1305
1324
  editor_command: str = "",
1325
+ prefix_warn_branches: str = "",
1306
1326
  ) -> None:
1307
1327
  super().__init__()
1308
1328
  self._cursor: int = 0
1309
1329
  self._default_branch: str = default_branch
1310
1330
  self._branch_prefix: bool = branch_prefix
1331
+ self._prefix_warn_branches: str = prefix_warn_branches
1311
1332
  self._draft_command: str = draft_command
1312
1333
  self._draft_prompt: str = draft_prompt
1313
1334
  self._parse_suggestions: bool = parse_suggestions
@@ -1522,6 +1543,12 @@ class SettingsDialog(ModalScreen["SettingsValues | None"]):
1522
1543
  "Use branch name as prefix in commit messages",
1523
1544
  )
1524
1545
  )
1546
+ lines.append(self._text_row(SETTINGS_ROW_PREFIX_WARN, " Warn on prefix: "))
1547
+ lines.append(
1548
+ f"[{theme.dialog_hint}]"
1549
+ " (branch names, separated by spaces, e.g. master main)"
1550
+ f"[/{theme.dialog_hint}]"
1551
+ )
1525
1552
  lines.append("")
1526
1553
  lines.append(self._section_header("Commit message editor"))
1527
1554
  lines.append("")
@@ -1600,6 +1627,7 @@ class SettingsDialog(ModalScreen["SettingsValues | None"]):
1600
1627
  parse_suggestions=self._parse_suggestions,
1601
1628
  editor=self._editor,
1602
1629
  editor_command=self._editor_command,
1630
+ prefix_warn_branches=self._prefix_warn_branches,
1603
1631
  )
1604
1632
 
1605
1633
 
@@ -2641,6 +2669,15 @@ class GitNightCommanderApp(App):
2641
2669
  ]
2642
2670
 
2643
2671
  CSS = """
2672
+ /* Warning notifications take the theme's own color for them instead of
2673
+ $warning, which a theme may have spent on its chrome. */
2674
+ Toast.-warning {
2675
+ border-left: outer $warning-notification;
2676
+ color: $warning-notification;
2677
+ }
2678
+ Toast.-warning .toast--title {
2679
+ color: $warning-notification;
2680
+ }
2644
2681
  Screen {
2645
2682
  layout: horizontal;
2646
2683
  layers: default overlay;
@@ -4014,6 +4051,12 @@ class GitNightCommanderApp(App):
4014
4051
  self._pending_commit_targets = list(targets)
4015
4052
  branch = self._commit_branch_prefix()
4016
4053
  prefill = f"{branch}: " if branch else ""
4054
+ warning = self._commit_prefix_warning(prefill)
4055
+ if warning:
4056
+ # Up front as well as in the question asked after the message:
4057
+ # the prefix is already known here, and this is before the
4058
+ # message has been written around it.
4059
+ self.notify(warning, severity="warning")
4017
4060
  self._edit_commit_message(prefill)
4018
4061
 
4019
4062
  def _edit_commit_message(self, message: str) -> None:
@@ -4287,7 +4330,29 @@ class GitNightCommanderApp(App):
4287
4330
  if message is None:
4288
4331
  self._commit_cancel()
4289
4332
  else:
4333
+ self._confirm_commit_prefix(message)
4334
+
4335
+ def _confirm_commit_prefix(self, message: str) -> None:
4336
+ """Ask about a branch prefix the settings warn against, before the
4337
+ commit confirmation rather than as a row inside it.
4338
+
4339
+ A question of its own has to be answered; a row under a list of
4340
+ commands only has to be overlooked. It comes here rather than on the
4341
+ message dialog because this is where the message is final, whichever
4342
+ editor wrote it. Y goes on to the commit confirmation, N reopens the
4343
+ message — the same way back that confirmation offers — and a message
4344
+ without such a prefix goes straight through.
4345
+ """
4346
+ warning = self._commit_prefix_warning(message)
4347
+ if not warning:
4290
4348
  self._confirm_commit(message)
4349
+ return
4350
+ self._show_confirm(
4351
+ f"{warning}. Continue?",
4352
+ [_commit_message_subject(message)],
4353
+ lambda: self._confirm_commit(message),
4354
+ lambda: self._edit_commit_message(message),
4355
+ )
4291
4356
 
4292
4357
  def _confirm_commit(self, message: str) -> None:
4293
4358
  """Show what the commit will run and ask before running it.
@@ -4315,6 +4380,21 @@ class GitNightCommanderApp(App):
4315
4380
  fill_height=True,
4316
4381
  )
4317
4382
 
4383
+ def _commit_prefix_warning(self, message: str) -> str:
4384
+ """The warning for a message prefixed with a branch named in the
4385
+ `commit_prefix_warn_branches` setting, or "" when it isn't.
4386
+
4387
+ One text for the notification the message editor opens with and the
4388
+ question asked once the message is written, so the two can't drift.
4389
+ """
4390
+ branches = _branch_names(
4391
+ _setting_str(self._load_settings(), "commit_prefix_warn_branches", "")
4392
+ )
4393
+ branch = _warned_commit_prefix(message, branches)
4394
+ if not branch:
4395
+ return ""
4396
+ return f"Warning: commit message is prefixed with branch '{branch}'"
4397
+
4318
4398
  def _commit_submit(self, message: str) -> None:
4319
4399
  result = self.git.commit_with_selection(self._pending_commit_targets, message)
4320
4400
  self._pending_commit_targets = []
@@ -4575,6 +4655,7 @@ class GitNightCommanderApp(App):
4575
4655
  parse_suggestions=bool(settings.get("commit_draft_parse_suggestions", False)),
4576
4656
  editor=_setting_str(settings, "commit_editor", COMMIT_EDITOR_BUILTIN),
4577
4657
  editor_command=_setting_str(settings, "commit_editor_command", ""),
4658
+ prefix_warn_branches=_setting_str(settings, "commit_prefix_warn_branches", ""),
4578
4659
  ),
4579
4660
  callback=self._settings_result,
4580
4661
  )
@@ -4592,6 +4673,7 @@ class GitNightCommanderApp(App):
4592
4673
  settings["commit_draft_parse_suggestions"] = values.parse_suggestions
4593
4674
  settings["commit_editor"] = values.editor
4594
4675
  settings["commit_editor_command"] = values.editor_command
4676
+ settings["commit_prefix_warn_branches"] = values.prefix_warn_branches
4595
4677
  if not self._save_settings(settings):
4596
4678
  self.notify("Settings saved (could not write to disk)")
4597
4679
  else:
@@ -89,6 +89,11 @@ menu_bar_foreground: $foreground
89
89
  footer_background: $panel
90
90
  footer_foreground: $foreground
91
91
 
92
+ # Warning notifications (the toast that says a commit message is about to be
93
+ # prefixed with a branch the settings warn against): its text and the bar on
94
+ # its left edge.
95
+ warning_notification: $warning
96
+
92
97
  # CSS variables emitted for the file-list cursor and multi-selection.
93
98
  # Values may reference Textual palette variables with `$name` (e.g. `$primary`).
94
99
  # References are resolved against this theme's own fields when the CSS is generated.
@@ -89,6 +89,11 @@ menu_bar_foreground: $foreground
89
89
  footer_background: $panel
90
90
  footer_foreground: $foreground
91
91
 
92
+ # Warning notifications (the toast that says a commit message is about to be
93
+ # prefixed with a branch the settings warn against): its text and the bar on
94
+ # its left edge.
95
+ warning_notification: $warning
96
+
92
97
  # CSS variables emitted for the file-list cursor and multi-selection.
93
98
  # Values may reference Textual palette variables with `$name` (e.g. `$primary`).
94
99
  # References are resolved against this theme's own fields when the CSS is generated.
@@ -97,6 +97,11 @@ menu_bar_foreground: $foreground
97
97
  footer_background: $panel
98
98
  footer_foreground: $foreground
99
99
 
100
+ # Warning notifications (the toast that says a commit message is about to be
101
+ # prefixed with a branch the settings warn against): its text and the bar on
102
+ # its left edge.
103
+ warning_notification: $warning
104
+
100
105
  # CSS variables emitted for the file-list cursor and multi-selection.
101
106
  # Values may reference Textual palette variables with `$name` (e.g. `$primary`).
102
107
  # References are resolved against this theme's own fields when the CSS is generated.
@@ -90,6 +90,11 @@ menu_bar_foreground: $foreground
90
90
  footer_background: $panel
91
91
  footer_foreground: $foreground
92
92
 
93
+ # Warning notifications (the toast that says a commit message is about to be
94
+ # prefixed with a branch the settings warn against): its text and the bar on
95
+ # its left edge.
96
+ warning_notification: $warning
97
+
93
98
  # CSS variables emitted for the file-list cursor and multi-selection.
94
99
  # Values may reference Textual palette variables with `$name` (e.g. `$primary`).
95
100
  # References are resolved against this theme's own fields when the CSS is generated.
@@ -97,6 +97,11 @@ menu_bar_foreground: $foreground
97
97
  footer_background: $panel
98
98
  footer_foreground: $foreground
99
99
 
100
+ # Warning notifications (the toast that says a commit message is about to be
101
+ # prefixed with a branch the settings warn against): its text and the bar on
102
+ # its left edge.
103
+ warning_notification: $warning
104
+
100
105
  # CSS variables emitted for the file-list cursor and multi-selection.
101
106
  # Values may reference Textual palette variables with `$name` (e.g. `$primary`).
102
107
  # References are resolved against this theme's own fields when the CSS is generated.
@@ -97,6 +97,11 @@ menu_bar_foreground: $foreground
97
97
  footer_background: $panel
98
98
  footer_foreground: $foreground
99
99
 
100
+ # Warning notifications (the toast that says a commit message is about to be
101
+ # prefixed with a branch the settings warn against): its text and the bar on
102
+ # its left edge.
103
+ warning_notification: $warning
104
+
100
105
  # CSS variables emitted for the file-list cursor and multi-selection.
101
106
  # Values may reference Textual palette variables with `$name` (e.g. `$primary`).
102
107
  # References are resolved against this theme's own fields when the CSS is generated.
@@ -90,6 +90,11 @@ menu_bar_foreground: $foreground
90
90
  footer_background: $panel
91
91
  footer_foreground: $foreground
92
92
 
93
+ # Warning notifications (the toast that says a commit message is about to be
94
+ # prefixed with a branch the settings warn against): its text and the bar on
95
+ # its left edge.
96
+ warning_notification: $warning
97
+
93
98
  # CSS variables emitted for the file-list cursor and multi-selection.
94
99
  # Values may reference Textual palette variables with `$name` (e.g. `$primary`).
95
100
  # References are resolved against this theme's own fields when the CSS is generated.
@@ -92,6 +92,11 @@ menu_bar_foreground: $foreground
92
92
  footer_background: $panel
93
93
  footer_foreground: $foreground
94
94
 
95
+ # Warning notifications (the toast that says a commit message is about to be
96
+ # prefixed with a branch the settings warn against): its text and the bar on
97
+ # its left edge.
98
+ warning_notification: $warning
99
+
95
100
  # CSS variables emitted for the file-list cursor and multi-selection.
96
101
  # Values may reference Textual palette variables with `$name` (e.g. `$primary`).
97
102
  # References are resolved against this theme's own fields when the CSS is generated.
@@ -91,6 +91,11 @@ menu_bar_foreground: $foreground
91
91
  footer_background: $panel
92
92
  footer_foreground: $foreground
93
93
 
94
+ # Warning notifications (the toast that says a commit message is about to be
95
+ # prefixed with a branch the settings warn against): its text and the bar on
96
+ # its left edge.
97
+ warning_notification: $warning
98
+
94
99
  # CSS variables emitted for the file-list cursor and multi-selection.
95
100
  # Values may reference Textual palette variables with `$name` (e.g. `$primary`).
96
101
  # References are resolved against this theme's own fields when the CSS is generated.
@@ -93,6 +93,13 @@ menu_bar_foreground: $background
93
93
  footer_background: $warning
94
94
  footer_foreground: $background
95
95
 
96
+ # Warning notifications (the toast that says a commit message is about to be
97
+ # prefixed with a branch the settings warn against): its text and the bar on
98
+ # its left edge. $warning is this theme's cyan — the color of the menu bar,
99
+ # which reads as chrome rather than as a warning — so the toast gets a red
100
+ # of its own.
101
+ warning_notification: '#FF5555'
102
+
96
103
  # CSS variables emitted for the file-list cursor and multi-selection.
97
104
  # Values may reference Textual palette variables with `$name` (e.g. `$primary`).
98
105
  # References are resolved against this theme's own fields when the CSS is generated.
@@ -91,6 +91,11 @@ menu_bar_foreground: $foreground
91
91
  footer_background: $panel
92
92
  footer_foreground: $foreground
93
93
 
94
+ # Warning notifications (the toast that says a commit message is about to be
95
+ # prefixed with a branch the settings warn against): its text and the bar on
96
+ # its left edge.
97
+ warning_notification: $warning
98
+
94
99
  # CSS variables emitted for the file-list cursor and multi-selection.
95
100
  # Values may reference Textual palette variables with `$name` (e.g. `$primary`).
96
101
  # References are resolved against this theme's own fields when the CSS is generated.
@@ -95,6 +95,11 @@ menu_bar_foreground: $foreground
95
95
  footer_background: $panel
96
96
  footer_foreground: $foreground
97
97
 
98
+ # Warning notifications (the toast that says a commit message is about to be
99
+ # prefixed with a branch the settings warn against): its text and the bar on
100
+ # its left edge.
101
+ warning_notification: $warning
102
+
98
103
  # CSS variables emitted for the file-list cursor and multi-selection.
99
104
  # Values may reference Textual palette variables with `$name` (e.g. `$primary`).
100
105
  # References are resolved against this theme's own fields when the CSS is generated.
@@ -96,6 +96,11 @@ menu_bar_foreground: $foreground
96
96
  footer_background: $panel
97
97
  footer_foreground: $foreground
98
98
 
99
+ # Warning notifications (the toast that says a commit message is about to be
100
+ # prefixed with a branch the settings warn against): its text and the bar on
101
+ # its left edge.
102
+ warning_notification: $warning
103
+
99
104
  # CSS variables emitted for the file-list cursor and multi-selection.
100
105
  # Values may reference Textual palette variables with `$name` (e.g. `$primary`).
101
106
  # References are resolved against this theme's own fields when the CSS is generated.
@@ -96,6 +96,11 @@ menu_bar_foreground: $foreground
96
96
  footer_background: $panel
97
97
  footer_foreground: $foreground
98
98
 
99
+ # Warning notifications (the toast that says a commit message is about to be
100
+ # prefixed with a branch the settings warn against): its text and the bar on
101
+ # its left edge.
102
+ warning_notification: $warning
103
+
99
104
  # CSS variables emitted for the file-list cursor and multi-selection.
100
105
  # Values may reference Textual palette variables with `$name` (e.g. `$primary`).
101
106
  # References are resolved against this theme's own fields when the CSS is generated.
@@ -96,6 +96,11 @@ menu_bar_foreground: $foreground
96
96
  footer_background: $panel
97
97
  footer_foreground: $foreground
98
98
 
99
+ # Warning notifications (the toast that says a commit message is about to be
100
+ # prefixed with a branch the settings warn against): its text and the bar on
101
+ # its left edge.
102
+ warning_notification: $warning
103
+
99
104
  # CSS variables emitted for the file-list cursor and multi-selection.
100
105
  # Values may reference Textual palette variables with `$name` (e.g. `$primary`).
101
106
  # References are resolved against this theme's own fields when the CSS is generated.
@@ -93,6 +93,11 @@ menu_bar_foreground: $foreground
93
93
  footer_background: $panel
94
94
  footer_foreground: $foreground
95
95
 
96
+ # Warning notifications (the toast that says a commit message is about to be
97
+ # prefixed with a branch the settings warn against): its text and the bar on
98
+ # its left edge.
99
+ warning_notification: $warning
100
+
96
101
  # CSS variables emitted for the file-list cursor and multi-selection.
97
102
  # Values may reference Textual palette variables with `$name` (e.g. `$primary`).
98
103
  # References are resolved against this theme's own fields when the CSS is generated.
@@ -93,6 +93,11 @@ menu_bar_foreground: $foreground
93
93
  footer_background: $panel
94
94
  footer_foreground: $foreground
95
95
 
96
+ # Warning notifications (the toast that says a commit message is about to be
97
+ # prefixed with a branch the settings warn against): its text and the bar on
98
+ # its left edge.
99
+ warning_notification: $warning
100
+
96
101
  # CSS variables emitted for the file-list cursor and multi-selection.
97
102
  # Values may reference Textual palette variables with `$name` (e.g. `$primary`).
98
103
  # References are resolved against this theme's own fields when the CSS is generated.
@@ -99,6 +99,11 @@ menu_bar_foreground: $foreground
99
99
  footer_background: $panel
100
100
  footer_foreground: $foreground
101
101
 
102
+ # Warning notifications (the toast that says a commit message is about to be
103
+ # prefixed with a branch the settings warn against): its text and the bar on
104
+ # its left edge.
105
+ warning_notification: $warning
106
+
102
107
  # CSS variables emitted for the file-list cursor and multi-selection.
103
108
  # Values may reference Textual palette variables with `$name` (e.g. `$primary`).
104
109
  # References are resolved against this theme's own fields when the CSS is generated.
@@ -89,6 +89,11 @@ menu_bar_foreground: $foreground
89
89
  footer_background: $panel
90
90
  footer_foreground: $foreground
91
91
 
92
+ # Warning notifications (the toast that says a commit message is about to be
93
+ # prefixed with a branch the settings warn against): its text and the bar on
94
+ # its left edge.
95
+ warning_notification: $warning
96
+
92
97
  # CSS variables emitted for the file-list cursor and multi-selection.
93
98
  # Values may reference Textual palette variables with `$name` (e.g. `$primary`).
94
99
  # References are resolved against this theme's own fields when the CSS is generated.
@@ -90,6 +90,11 @@ menu_bar_foreground: $foreground
90
90
  footer_background: $panel
91
91
  footer_foreground: $foreground
92
92
 
93
+ # Warning notifications (the toast that says a commit message is about to be
94
+ # prefixed with a branch the settings warn against): its text and the bar on
95
+ # its left edge.
96
+ warning_notification: $warning
97
+
93
98
  # CSS variables emitted for the file-list cursor and multi-selection.
94
99
  # Values may reference Textual palette variables with `$name` (e.g. `$primary`).
95
100
  # References are resolved against this theme's own fields when the CSS is generated.
@@ -90,6 +90,11 @@ menu_bar_foreground: $foreground
90
90
  footer_background: $panel
91
91
  footer_foreground: $foreground
92
92
 
93
+ # Warning notifications (the toast that says a commit message is about to be
94
+ # prefixed with a branch the settings warn against): its text and the bar on
95
+ # its left edge.
96
+ warning_notification: $warning
97
+
93
98
  # CSS variables emitted for the file-list cursor and multi-selection.
94
99
  # Values may reference Textual palette variables with `$name` (e.g. `$primary`).
95
100
  # References are resolved against this theme's own fields when the CSS is generated.
@@ -117,6 +117,13 @@ class UITheme:
117
117
  footer_background: str = "$panel"
118
118
  footer_foreground: str = "$foreground"
119
119
 
120
+ # CSS-side color of a warning notification: the toast's text and the bar
121
+ # on its left edge. The default is the palette's own warning role, which
122
+ # is what Textual paints that bar in anyway; a theme that has spent
123
+ # $warning on something else (midnight-commander's cyan chrome) names a
124
+ # color that still reads as a warning.
125
+ warning_notification: str = "$warning"
126
+
120
127
  # CSS-side palette for the file list cursor + multi-selection.
121
128
  cursor_row_background: str = "$primary"
122
129
  cursor_row_foreground: str = "$foreground"
@@ -184,6 +191,7 @@ class UITheme:
184
191
  "menu-bar-foreground": self._resolve_refs(self.menu_bar_foreground),
185
192
  "footer-background": self._resolve_refs(self.footer_background),
186
193
  "footer-foreground": self._resolve_refs(self.footer_foreground),
194
+ "warning-notification": self._resolve_refs(self.warning_notification),
187
195
  "cursor-row-background": self._resolve_refs(self.cursor_row_background),
188
196
  "cursor-row-foreground": self._resolve_refs(self.cursor_row_foreground),
189
197
  "selected-file-background": self._resolve_refs(self.selected_file_background),
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "gitnc"
7
- version = "0.1.3"
7
+ version = "0.1.4"
8
8
  description = "GitNightCommander — a TUI git log browser built with Textual"
9
9
  requires-python = ">=3.12"
10
10
  dependencies = [
@@ -44,6 +44,7 @@ fmt = [
44
44
  "ruff format gitnc tests",
45
45
  ]
46
46
  start = "python -m gitnc {args}"
47
+ rebuild = "hatch build --clean {args}"
47
48
 
48
49
  [tool.ruff]
49
50
  line-length = 100
@@ -346,6 +346,7 @@ try:
346
346
  SETTINGS_ROW_EDITOR_COMMAND_LINE,
347
347
  SETTINGS_ROW_EDITOR_ENVIRONMENT,
348
348
  SETTINGS_ROW_PARSE,
349
+ SETTINGS_ROW_PREFIX_WARN,
349
350
  SETTINGS_ROW_PROMPT,
350
351
  SETTINGS_SECTION_WIDTH,
351
352
  SHORTCUT_ALIASES,
@@ -441,6 +442,7 @@ except ModuleNotFoundError as exc:
441
442
  SETTINGS_ROW_EDITOR_COMMAND_LINE,
442
443
  SETTINGS_ROW_EDITOR_ENVIRONMENT,
443
444
  SETTINGS_ROW_PARSE,
445
+ SETTINGS_ROW_PREFIX_WARN,
444
446
  SETTINGS_ROW_PROMPT,
445
447
  SETTINGS_SECTION_WIDTH,
446
448
  SHORTCUT_ALIASES,
@@ -3394,6 +3396,36 @@ class TestSettingsDialog(unittest.TestCase):
3394
3396
  self.assertEqual(dialog.get_values().draft_prompt, "a ")
3395
3397
  self.assertFalse(dialog.get_values().branch_prefix)
3396
3398
 
3399
+ def test_the_warned_prefixes_are_typed_under_the_prefix_checkbox(self):
3400
+ dialog = SettingsDialog(prefix_warn_branches="master")
3401
+ for _ in range(SETTINGS_ROW_PREFIX_WARN):
3402
+ dialog.on_key(_key_event("down"))
3403
+ self.assertEqual(dialog._cursor, SETTINGS_ROW_PREFIX_WARN)
3404
+
3405
+ for key, character in (("space", None), ("m", "m")):
3406
+ dialog.on_key(_key_event(key, character=character))
3407
+
3408
+ self.assertEqual(dialog.get_values().prefix_warn_branches, "master m")
3409
+ self.assertFalse(dialog.get_values().branch_prefix)
3410
+
3411
+ def test_the_warned_prefixes_round_trip_through_the_settings_file(self):
3412
+ app = _mock_app()
3413
+ with patch.object(
3414
+ app, "_load_settings", return_value={"commit_prefix_warn_branches": "master main"}
3415
+ ):
3416
+ app.action_settings()
3417
+ values = _pushed_screen(app).get_values()
3418
+ self.assertEqual(values.prefix_warn_branches, "master main")
3419
+
3420
+ saved: dict[str, object] = {}
3421
+ with (
3422
+ patch.object(app, "_load_settings", return_value={}),
3423
+ patch.object(app, "_save_settings", side_effect=lambda s: saved.update(s) or True),
3424
+ ):
3425
+ app._settings_result(values)
3426
+
3427
+ self.assertEqual(saved["commit_prefix_warn_branches"], "master main")
3428
+
3397
3429
  def test_cursor_wraps_over_every_row(self):
3398
3430
  dialog = SettingsDialog()
3399
3431
 
@@ -4681,6 +4713,7 @@ class TestUIThemeCssVariables(unittest.TestCase):
4681
4713
  "menu-bar-foreground",
4682
4714
  "footer-background",
4683
4715
  "footer-foreground",
4716
+ "warning-notification",
4684
4717
  "cursor-row-background",
4685
4718
  "cursor-row-foreground",
4686
4719
  "selected-file-background",
@@ -4722,6 +4755,28 @@ class TestUIThemeCssVariables(unittest.TestCase):
4722
4755
  self.assertNotIn("$", variables[field_name])
4723
4756
 
4724
4757
 
4758
+ class TestWarningNotificationThemeColor(unittest.TestCase):
4759
+ """A warning toast is painted from the theme's `warning_notification`
4760
+ field rather than from `$warning`, which a theme may use for chrome."""
4761
+
4762
+ def test_the_default_is_the_palette_warning_role(self):
4763
+ from gitnc.ui_theme import UITheme
4764
+
4765
+ theme = UITheme(name="plain", primary="#123456", warning="#abcdef")
4766
+
4767
+ self.assertEqual(theme.css_variables()["warning-notification"], "#abcdef")
4768
+
4769
+ def test_midnight_commander_warns_in_red_not_in_its_chrome_cyan(self):
4770
+ css_vars = MIDNIGHT_COMMANDER_THEME.css_variables()
4771
+
4772
+ self.assertEqual(css_vars["warning-notification"], "#FF5555")
4773
+ self.assertNotEqual(css_vars["warning-notification"], MIDNIGHT_COMMANDER_THEME.warning)
4774
+
4775
+ def test_the_app_css_paints_warning_toasts_from_the_variable(self):
4776
+ self.assertIn("border-left: outer $warning-notification;", GitNightCommanderApp.CSS)
4777
+ self.assertIn("color: $warning-notification;", GitNightCommanderApp.CSS)
4778
+
4779
+
4725
4780
  class TestTextSelectionThemeColors(unittest.TestCase):
4726
4781
  """Selected text in the commit message box takes its colors from the
4727
4782
  theme's text selection fields, emitted as Textual's own
@@ -5195,6 +5250,97 @@ class TestCommitFlow(unittest.TestCase):
5195
5250
  self.assertTrue(dialogs.pushed[-1].fill_height)
5196
5251
  app.git.commit.assert_not_called()
5197
5252
 
5253
+ def _warned_app(self):
5254
+ entry = GitEntry(GitCode.MODIFIED_UNMODIFIED, "pkg/a.py")
5255
+ app, dialogs, _ = self._app([entry], staged={"pkg/a.py"})
5256
+ app._load_settings = MagicMock(return_value={"commit_prefix_warn_branches": "main, master"})
5257
+ app.action_commit_files()
5258
+ return app, dialogs
5259
+
5260
+ def test_a_warned_prefix_is_asked_about_before_the_commit_confirmation(self):
5261
+ app, dialogs = self._warned_app()
5262
+
5263
+ dialogs._resolve("master: fix x")
5264
+
5265
+ self.assertEqual(
5266
+ dialogs.prompt(),
5267
+ "Warning: commit message is prefixed with branch 'master'. Continue?",
5268
+ )
5269
+ self.assertEqual(dialogs.commands(), ["master: fix x"])
5270
+ app.git.commit.assert_not_called()
5271
+
5272
+ def test_accepting_the_prefix_warning_goes_on_to_the_commit_confirmation(self):
5273
+ app, dialogs = self._warned_app()
5274
+ dialogs._resolve("master: fix x")
5275
+
5276
+ dialogs.confirm()
5277
+
5278
+ self.assertEqual(dialogs.prompt(), "Commit 1 file(s)?")
5279
+ self.assertEqual(dialogs.commands(), ["git commit -m master: fix x"])
5280
+ app.git.commit.assert_not_called()
5281
+
5282
+ def test_declining_the_prefix_warning_reopens_the_message(self):
5283
+ app, dialogs = self._warned_app()
5284
+ dialogs._resolve("master: fix x")
5285
+
5286
+ dialogs.cancel()
5287
+
5288
+ reopened = dialogs.pushed[-1]
5289
+ self.assertIsInstance(reopened, CommitDialog)
5290
+ self.assertEqual(reopened.prefill, "master: fix x")
5291
+ app.git.commit.assert_not_called()
5292
+
5293
+ def test_a_warned_prefix_is_announced_when_the_message_editor_opens(self):
5294
+ entry = GitEntry(GitCode.MODIFIED_UNMODIFIED, "pkg/a.py")
5295
+ app, dialogs, _ = self._app([entry], staged={"pkg/a.py"}, branch="master")
5296
+ app._load_settings = MagicMock(
5297
+ return_value={
5298
+ "branch_prefix_in_commits": True,
5299
+ "commit_prefix_warn_branches": "master",
5300
+ }
5301
+ )
5302
+
5303
+ with patch.object(app, "notify") as notify:
5304
+ app.action_commit_files()
5305
+
5306
+ self.assertEqual(self._commit_screen(dialogs).prefill, "master: ")
5307
+ notify.assert_called_once_with(
5308
+ "Warning: commit message is prefixed with branch 'master'", severity="warning"
5309
+ )
5310
+
5311
+ def test_an_unlisted_prefix_opens_the_message_editor_quietly(self):
5312
+ entry = GitEntry(GitCode.MODIFIED_UNMODIFIED, "pkg/a.py")
5313
+ app, dialogs, _ = self._app([entry], staged={"pkg/a.py"}, branch="feature")
5314
+ app._load_settings = MagicMock(
5315
+ return_value={
5316
+ "branch_prefix_in_commits": True,
5317
+ "commit_prefix_warn_branches": "master",
5318
+ }
5319
+ )
5320
+
5321
+ with patch.object(app, "notify") as notify:
5322
+ app.action_commit_files()
5323
+
5324
+ self.assertEqual(self._commit_screen(dialogs).prefill, "feature: ")
5325
+ notify.assert_not_called()
5326
+
5327
+ def test_other_prefixes_go_straight_to_the_commit_confirmation(self):
5328
+ """Only a whole listed name followed by the colon counts — not a
5329
+ branch that merely starts with one, and not the name mid-message."""
5330
+ entry = GitEntry(GitCode.MODIFIED_UNMODIFIED, "pkg/a.py")
5331
+ for message in ("master-fix: x", "feature: merge master: x", "masterful change"):
5332
+ with self.subTest(message=message):
5333
+ app, dialogs, _ = self._app([entry], staged={"pkg/a.py"})
5334
+ app._load_settings = MagicMock(
5335
+ return_value={"commit_prefix_warn_branches": "master"}
5336
+ )
5337
+ app.action_commit_files()
5338
+
5339
+ dialogs._resolve(message)
5340
+
5341
+ self.assertEqual(dialogs.prompt(), "Commit 1 file(s)?")
5342
+ self.assertEqual(dialogs.commands(), [f"git commit -m {message}"])
5343
+
5198
5344
  def test_the_confirmation_shows_only_the_subject_of_a_long_message(self):
5199
5345
  """The popup renders one command per row, so a multi-line `-m` has to
5200
5346
  be cut down to its first line — marked as cut."""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes