Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<p align="center">
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge&logo=opensourceinitiative&logoColor=FFFFFF" alt="License"></a>
<img src="https://img.shields.io/badge/Python-3.10%2B-blue?style=for-the-badge&logo=python&logoColor=white" alt="Python">
<img src="https://img.shields.io/badge/Textual-0.80%2B-8A2BE2?style=for-the-badge" alt="Textual">
<img src="https://img.shields.io/badge/Textual-8.2.5%2B-8A2BE2?style=for-the-badge" alt="Textual">
<img src="https://img.shields.io/badge/Platform-Linux%20%7C%20macOS%20%7C%20Windows-lightgrey?style=for-the-badge&logo=linux&logoColor=FCC624" alt="Platform">
<img src="https://img.shields.io/badge/code%20style-black-000000?style=for-the-badge" alt="black">
<img src="https://img.shields.io/badge/linting-ruff-orange?style=for-the-badge" alt="ruff">
Expand Down
10 changes: 9 additions & 1 deletion src/rigi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,18 @@
from rigi.widgets.help_panel import RigiShortcutsBar, extract_help_annotation
from rigi.widgets.image import RigiImage, TerminalImageProtocol, detect_image_protocol
from rigi.widgets.mouse import RigiClickable, RigiDraggable, RigiMouseMixin
from rigi.widgets.notifications import (
RigiNotificationRack as RigiNotificationRack,
)
from rigi.widgets.notifications import (
RigiNotificationWidget as RigiNotificationWidget,
)
from rigi.widgets.settings_screen import RigiSettingDef, RigiSettingsScreen
from rigi.widgets.sidebar import RigiSidebar
from rigi.widgets.statusbar import RigiStatusBar, RigiStatusItem
from rigi.widgets.terminal_bar import RigiTerminalBar

__version__ = "1.0.0"
__version__ = "1.1.0"
__all__ = [
# Textual primitives
"Widget",
Expand Down Expand Up @@ -126,6 +132,8 @@
"extract_help_annotation",
"RigiGauge",
"RigiSparkline",
"RigiNotificationRack",
"RigiNotificationWidget",
# Platform utilities
"platform",
"console",
Expand Down
56 changes: 50 additions & 6 deletions src/rigi/core/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from textual import on
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.notifications import SeverityLevel
from textual.widget import Widget

from rigi.commands.command import Command
Expand All @@ -32,6 +33,7 @@
from rigi.widgets.content_area import RigiContentArea
from rigi.widgets.hamburger_menu import RigiMenuItemData
from rigi.widgets.help_panel import RigiShortcutsBar, extract_help_annotation
from rigi.widgets.notifications import RigiNotificationRack
from rigi.widgets.sidebar import RigiSidebar
from rigi.widgets.statusbar import (
RigiStatusBar,
Expand Down Expand Up @@ -119,6 +121,7 @@ def __init__(
self._rigi_menu_items: list[tuple[str, str, Callable[[], None]]] = []
self._rigi_settings: list[RigiSettingDef] = []

self._disable_notifications = True
self._register_builtin_commands()

def _register_builtin_commands(self) -> None:
Expand Down Expand Up @@ -260,6 +263,7 @@ def compose(self) -> ComposeResult:
registry=self._cmd_registry,
history_file=history_file,
)
yield RigiNotificationRack()

def on_mount(self) -> None:
self.title = f"{self._prog_name} v{self._version}"
Expand Down Expand Up @@ -296,9 +300,27 @@ def _set_terminal_title(self) -> None:
except Exception:
pass

def notify(
self,
message: str,
*,
title: str = "",
severity: SeverityLevel = "information",
timeout: float | None = 5.0,
markup: bool = True,
) -> None:
if not markup:
message = message.replace("[", "\\[")
effective_timeout = timeout if timeout is not None else 5.0
try:
self.query_one(RigiNotificationRack).add_notification(
title, message, severity, effective_timeout
)
except Exception:
pass

@property
def terminal(self) -> str:
"""Name of the running terminal (kitty, iterm2, wezterm, etc.)."""
return _console.detect_terminal()

@property
Expand Down Expand Up @@ -455,8 +477,8 @@ async def _run_shell(self, cmd: str) -> None:
)
_terminal_log.error(f"Shell command timed out: {cmd}")
return
out = (stdout.decode(errors="replace") + stderr.decode(errors="replace")).strip()
display = out[:1200] if out else "(no output)"
raw = (stdout.decode(errors="replace") + stderr.decode(errors="replace")).strip()
display = (raw[:1200] if raw else "(no output)").replace("[", "\\[")
_terminal_log.info(f"Shell command completed: {cmd}")
try:
self.query_one(RigiBottomPanel).write_output(display)
Expand All @@ -465,10 +487,11 @@ async def _run_shell(self, cmd: str) -> None:
except Exception as exc:
msg = str(exc)
_terminal_log.error(f"Shell command failed: {cmd}", exc_info=True)
safe_msg = msg.replace("[", "\\[")
try:
self.query_one(RigiBottomPanel).write_output(f"[red]{msg}[/red]")
self.query_one(RigiBottomPanel).write_output(f"[red]{safe_msg}[/red]")
except Exception:
self.notify(msg, severity="error", title=f"$ {cmd[:30]}")
self.notify(safe_msg, severity="error", title=f"$ {cmd[:30]}")

@on(_HamburgerButton.Clicked)
def on_hamburger_clicked(self, event: _HamburgerButton.Clicked) -> None:
Expand Down Expand Up @@ -723,8 +746,29 @@ def add_setting(
value_fn: Callable[[], str] | None = None,
action_fn: Callable[[], None] | None = None,
action_label: str = "Change",
write_fn: Callable[[str], None] | None = None,
) -> RigiSettingDef:
s = RigiSettingDef(
category, label, description, value_fn, action_fn, action_label, write_fn
)
self._rigi_settings.append(s)
return s

def add_checkbox_setting(
self,
category: str,
label: str,
description: str = "",
checked_fn: Callable[[], bool] | None = None,
toggle_fn: Callable[[], None] | None = None,
) -> RigiSettingDef:
s = RigiSettingDef(category, label, description, value_fn, action_fn, action_label)
s = RigiSettingDef(
category=category,
label=label,
description=description,
checkbox_fn=checked_fn,
toggle_fn=toggle_fn,
)
self._rigi_settings.append(s)
return s

Expand Down
85 changes: 84 additions & 1 deletion src/rigi/css/default.tcss
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,14 @@ _SettingItem {
_SettingItem ._s-label { color: #c9d1d9; text-style: bold; height: 1; }
_SettingItem ._s-desc { color: #6e7681; height: 1; }

_SettingSwitch {
height: 1;
width: auto;
margin-top: 1;
background: transparent;
}
_SettingSwitch Switch { height: 1; width: auto; }

_SettingsContent { width: 1fr; height: 100%; padding: 1 2; overflow-y: auto; background: transparent; }
_SettingsContent ._cat-title { color: #58a6ff; text-style: bold; height: 1; margin-bottom: 1; }

Expand All @@ -336,11 +344,30 @@ RigiSettingsScreen { align: center middle; background: transparent; }
}
#s-titlebar {
height: 2;
padding: 0 2;
padding: 0 1;
border-bottom: solid #21262d;
background: transparent;
layout: horizontal;
content-align: left middle;
}
#s-title-lbl {
width: 1fr;
height: 1;
color: #c9d1d9;
content-align: left middle;
padding: 0 1;
}
#s-close-btn {
width: 3;
height: 1;
min-width: 3;
max-width: 3;
border: none;
background: transparent;
color: #6e7681;
padding: 0;
}
#s-close-btn:hover { color: #f85149; background: transparent; }
#s-body { layout: horizontal; height: 1fr; background: transparent; }
#s-categories {
width: 22;
Expand Down Expand Up @@ -439,3 +466,59 @@ RigiBottomPanel #terminal-input {
color: #e6edf3;
}
RigiBottomPanel #terminal-input:focus { border: none; }

RigiNotificationRack {
layer: overlay;
width: 1fr;
height: auto;
max-height: 80%;
dock: bottom;
align: right bottom;
margin-bottom: 3;
background: transparent;
layout: vertical;
}

RigiNotificationWidget {
width: 44;
max-width: 50%;
height: auto;
min-height: 3;
margin: 0 1 1 0;
padding: 0 1 1 1;
background: #161b22;
border-left: thick #58a6ff;
}
RigiNotificationWidget.notif--warning { border-left: thick #e3b341; }
RigiNotificationWidget.notif--error { border-left: thick #f85149; }

.notif-header {
layout: horizontal;
height: 1;
width: 100%;
background: transparent;
}
.notif-title {
width: 1fr;
height: 1;
content-align: left middle;
background: transparent;
}
.notif-message {
width: 100%;
height: auto;
color: #c9d1d9;
background: transparent;
margin-top: 1;
}
.notif-close {
width: 3;
min-width: 3;
max-width: 3;
height: 1;
border: none;
background: transparent;
color: #6e7681;
padding: 0;
}
.notif-close:hover { color: #f85149; background: transparent; }
51 changes: 43 additions & 8 deletions src/rigi/screens/settings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
"""RigiSettingsScreen — modal settings panel."""

from __future__ import annotations

import logging
Expand All @@ -11,7 +9,7 @@
from textual.message import Message
from textual.screen import ModalScreen
from textual.widget import Widget
from textual.widgets import Input, Label
from textual.widgets import Button, Input, Label, Switch

_ui_log = logging.getLogger("rigi.ui")

Expand All @@ -26,6 +24,8 @@ def __init__(
action_fn: Callable[[], None] | None = None,
action_label: str = "Change",
write_fn: Callable[[str], None] | None = None,
checkbox_fn: Callable[[], bool] | None = None,
toggle_fn: Callable[[], None] | None = None,
) -> None:
self.category = category
self.label = label
Expand All @@ -34,6 +34,8 @@ def __init__(
self.action_fn = action_fn
self.action_label = action_label
self.write_fn = write_fn
self.checkbox_fn = checkbox_fn
self.toggle_fn = toggle_fn
self._current_value: str | None = None

def get_value(self) -> str:
Expand All @@ -55,6 +57,15 @@ def set_value(self, v: str) -> None:
except Exception as e:
_ui_log.error(f"Error setting value for {self.label}: {e}", exc_info=True)

def get_checked(self) -> bool:
if self.checkbox_fn is None:
return False
try:
return self.checkbox_fn()
except Exception as e:
_ui_log.error(f"Error getting checkbox value for {self.label}: {e}", exc_info=True)
return False


class _CategoryClicked(Message):
def __init__(self, name: str) -> None:
Expand Down Expand Up @@ -130,6 +141,24 @@ def on_submitted(self, event: Input.Submitted) -> None:
self.app.set_focus(None)


class _SettingSwitch(Widget):
def __init__(self, setting: RigiSettingDef) -> None:
super().__init__()
self._setting = setting

def compose(self) -> ComposeResult:
yield Switch(value=self._setting.get_checked(), classes="_s-switch")

@on(Switch.Changed)
def on_changed(self, event: Switch.Changed) -> None:
event.stop()
if self._setting.toggle_fn:
try:
self._setting.toggle_fn()
except Exception as e:
_ui_log.error(f"Error toggling setting {self._setting.label}: {e}", exc_info=True)


class _SettingItem(Widget):
def __init__(self, setting: RigiSettingDef) -> None:
super().__init__()
Expand All @@ -139,11 +168,11 @@ def compose(self) -> ComposeResult:
yield Label(self._setting.label, classes="_s-label")
if self._setting.description:
yield Label(self._setting.description, classes="_s-desc")
if self._setting.write_fn is not None or (
self._setting.value_fn is not None and self._setting.action_fn is None
):
if self._setting.checkbox_fn is not None:
yield _SettingSwitch(self._setting)
elif self._setting.write_fn is not None:
yield _SettingInput(self._setting)
elif self._setting.value_fn or self._setting.action_fn:
elif self._setting.value_fn is not None or self._setting.action_fn is not None:
yield _ValueRow(self._setting)


Expand Down Expand Up @@ -171,7 +200,8 @@ def __init__(self, settings: list[RigiSettingDef]) -> None:
def compose(self) -> ComposeResult:
with Widget(id="s-outer"):
with Widget(id="s-titlebar"):
yield Label("[dim]ESC to close[/dim]")
yield Label("Settings", id="s-title-lbl")
yield Button("×", id="s-close-btn")
with Widget(id="s-body"):
with Widget(id="s-categories"):
for cat in self._categories:
Expand All @@ -185,6 +215,11 @@ def on_mount(self) -> None:
self.query_one("#s-outer").border_title = "⚙ Settings"
self._render_category(self._active_category)

@on(Button.Pressed, "#s-close-btn")
def on_close_pressed(self, event: Button.Pressed) -> None:
event.stop()
self.dismiss(None)

@on(_CategoryClicked)
def on_category_clicked(self, event: _CategoryClicked) -> None:
event.stop()
Expand Down
Loading
Loading