Download app.py from smlflg/SidecarCommando: direct link, hf CLI and curl.
- Browser
- Download file 26.9 kB
-
https://huggingface.co/smlflg/SidecarCommando/resolve/main/app.py
- Command line
-
hf download hf://smlflg/SidecarCommando/app.py
-
curl -L -o app.py https://huggingface.co/smlflg/SidecarCommando/resolve/main/app.py
26.9 kB
| #!/usr/bin/env python3 | |
| """Native Linux AI Cockpit: JSON state renderer + event writer.""" | |
| from __future__ import annotations | |
| import sys | |
| from typing import Any | |
| import gi | |
| from state_store import StateStore | |
| gi.require_version("Adw", "1") | |
| gi.require_version("Gtk", "4.0") | |
| from gi.repository import Adw, Gio, Gtk, Pango # noqa: E402 | |
| APP_ID = "dev.sidecarcommando.AICockpit" | |
| SIDECAR_ICONS = { | |
| "focus": "system-run-symbolic", | |
| "scope": "media-playlist-shuffle-symbolic", | |
| "finish": "emblem-ok-symbolic", | |
| "reality": "edit-find-symbolic", | |
| "regulation": "preferences-system-symbolic", | |
| } | |
| CSS = """ | |
| window { | |
| background: #030b13; | |
| color: #f3f7ff; | |
| } | |
| .app-shell { | |
| background: radial-gradient(circle at 72% 0%, rgba(74, 42, 120, 0.16), transparent 34%), | |
| linear-gradient(135deg, #030912, #081522 46%, #06101a); | |
| } | |
| .sidebar { | |
| padding: 22px 14px 16px 14px; | |
| background: rgba(4, 13, 22, 0.94); | |
| border-right: 1px solid rgba(144, 166, 196, 0.18); | |
| } | |
| .main { | |
| padding: 18px 22px 18px 22px; | |
| } | |
| .brand-title { | |
| font-size: 23px; | |
| font-weight: 800; | |
| } | |
| .brand-subtitle, | |
| .muted { | |
| color: #a7b2c3; | |
| } | |
| .nav-item { | |
| padding: 12px 14px; | |
| border-radius: 8px; | |
| color: #aab6c8; | |
| } | |
| .nav-item-active { | |
| color: #bd8cff; | |
| background: rgba(108, 72, 170, 0.17); | |
| border-left: 2px solid #8e5aff; | |
| } | |
| .nav-icon, | |
| .work-icon { | |
| color: #9aa9bd; | |
| } | |
| .sidebar-focus { | |
| padding: 14px; | |
| border-radius: 8px; | |
| background: rgba(11, 24, 39, 0.82); | |
| border: 1px solid rgba(144, 166, 196, 0.16); | |
| } | |
| .sidebar-focus-dot { | |
| color: #59e17b; | |
| } | |
| .sidebar-focus-name { | |
| font-weight: 800; | |
| color: #f3f7ff; | |
| } | |
| .focus-panel, | |
| .lead-card, | |
| .sidecar-card, | |
| .next-step, | |
| .work-panel, | |
| .work-section, | |
| .code-panel { | |
| border: 1px solid rgba(144, 166, 196, 0.24); | |
| border-radius: 8px; | |
| background: rgba(13, 25, 40, 0.92); | |
| box-shadow: 0 12px 32px rgba(0, 0, 0, 0.24); | |
| } | |
| .focus-panel { | |
| padding: 18px 20px; | |
| } | |
| .focus-target { | |
| border-left: 1px solid rgba(144, 166, 196, 0.18); | |
| padding-left: 22px; | |
| } | |
| .work-panel { | |
| padding: 14px; | |
| } | |
| .eyebrow { | |
| color: #9b6dff; | |
| font-weight: 800; | |
| font-size: 12px; | |
| letter-spacing: 0.06em; | |
| } | |
| .focus-subtitle { | |
| color: #d4dbea; | |
| font-size: 17px; | |
| } | |
| .focus-goal { | |
| color: #59e17b; | |
| font-size: 13px; | |
| font-weight: 800; | |
| } | |
| .focus-action-button, | |
| .owner-button { | |
| padding: 8px 12px; | |
| border-radius: 7px; | |
| background: rgba(36, 50, 68, 0.74); | |
| border: 1px solid rgba(144, 166, 196, 0.22); | |
| } | |
| .section-title { | |
| font-size: 14px; | |
| font-weight: 800; | |
| color: #9b6dff; | |
| } | |
| .field-label { | |
| color: #9aa9bd; | |
| font-size: 12px; | |
| font-weight: 700; | |
| } | |
| entry, | |
| textview { | |
| color: #edf4ff; | |
| background: #0b1725; | |
| border: 1px solid rgba(144, 166, 196, 0.22); | |
| border-radius: 8px; | |
| } | |
| .focus-entry { | |
| color: #f3f7ff; | |
| background: transparent; | |
| border: 0; | |
| box-shadow: none; | |
| padding: 0; | |
| font-size: 30px; | |
| font-weight: 800; | |
| } | |
| .code-panel { | |
| background: rgba(7, 17, 29, 0.96); | |
| } | |
| .code-header { | |
| padding: 10px 12px; | |
| border-bottom: 1px solid rgba(144, 166, 196, 0.18); | |
| } | |
| .code-text { | |
| font-family: monospace; | |
| color: #cdd8e7; | |
| padding: 12px; | |
| font-size: 14px; | |
| } | |
| .work-section { | |
| padding: 12px; | |
| } | |
| .todo-row { | |
| color: #d7e0ee; | |
| } | |
| .todo-done { | |
| color: #59e17b; | |
| } | |
| .action-grid { | |
| margin-top: 2px; | |
| } | |
| .work-footer { | |
| color: #8f9cad; | |
| font-size: 12px; | |
| } | |
| .lead-card { | |
| padding: 22px 20px; | |
| border-color: #9b6dff; | |
| background: linear-gradient(135deg, rgba(44, 22, 79, 0.84), rgba(11, 21, 35, 0.96)); | |
| } | |
| .lead-title { | |
| font-size: 26px; | |
| font-weight: 800; | |
| } | |
| .lead-copy { | |
| font-size: 16px; | |
| line-height: 1.55; | |
| } | |
| .sidecar-card { | |
| padding: 18px; | |
| min-height: 194px; | |
| } | |
| .sidecar-title { | |
| font-size: 19px; | |
| font-weight: 800; | |
| } | |
| .sidecar-copy { | |
| color: #d8e1ee; | |
| font-size: 14px; | |
| line-height: 1.45; | |
| } | |
| .badge { | |
| border-radius: 7px; | |
| padding: 5px 10px; | |
| font-size: 12px; | |
| font-weight: 800; | |
| } | |
| .badge-hard { | |
| color: #bd8cff; | |
| border: 1px solid #8e5aff; | |
| background: rgba(98, 57, 160, 0.18); | |
| } | |
| .badge-medium { | |
| color: #59e17b; | |
| border: 1px solid #2bb457; | |
| background: rgba(43, 180, 87, 0.14); | |
| } | |
| .badge-low { | |
| color: #4da9ff; | |
| border: 1px solid #2580d7; | |
| background: rgba(37, 128, 215, 0.13); | |
| } | |
| .sidecar-icon { | |
| min-width: 48px; | |
| min-height: 48px; | |
| border-radius: 999px; | |
| border: 1px solid currentColor; | |
| background: rgba(255, 255, 255, 0.04); | |
| } | |
| .lead-card .sidecar-icon { | |
| min-width: 58px; | |
| min-height: 58px; | |
| } | |
| .sidecar-focus { | |
| color: #bd8cff; | |
| border-color: #9b6dff; | |
| } | |
| .sidecar-scope { | |
| color: #59e17b; | |
| border-color: #2bb457; | |
| background: linear-gradient(135deg, rgba(20, 71, 50, 0.48), rgba(11, 24, 32, 0.95)); | |
| } | |
| .sidecar-finish { | |
| color: #f4d044; | |
| border-color: #d5b900; | |
| background: linear-gradient(135deg, rgba(82, 68, 13, 0.38), rgba(16, 25, 27, 0.95)); | |
| } | |
| .sidecar-reality { | |
| color: #4da9ff; | |
| border-color: #2580d7; | |
| background: linear-gradient(135deg, rgba(18, 57, 92, 0.42), rgba(10, 22, 34, 0.95)); | |
| } | |
| .sidecar-regulation { | |
| color: #ff8a25; | |
| border-color: #e56f00; | |
| background: linear-gradient(135deg, rgba(87, 42, 8, 0.36), rgba(19, 22, 24, 0.95)); | |
| } | |
| .next-step { | |
| padding: 16px 20px; | |
| border-color: #2387d6; | |
| background: rgba(9, 35, 58, 0.92); | |
| } | |
| .next-label { | |
| color: #4da9ff; | |
| font-size: 13px; | |
| font-weight: 800; | |
| } | |
| .next-copy { | |
| font-size: 18px; | |
| font-weight: 650; | |
| } | |
| .hint { | |
| color: #8793a4; | |
| font-size: 12px; | |
| } | |
| """ | |
| def text(value: Any, fallback: str = "") -> str: | |
| return value if isinstance(value, str) else fallback | |
| def set_text_view(view: Gtk.TextView, value: str) -> None: | |
| view.get_buffer().set_text(value) | |
| def focus_name(state: dict[str, Any]) -> str: | |
| return text(state.get("active_focus", {}).get("name"), "miniZentrale") | |
| class SidecarCard(Gtk.Box): | |
| def __init__(self, sidecar: dict[str, Any], lead: bool = False) -> None: | |
| super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=12) | |
| self.sidecar = sidecar | |
| self.lead = lead | |
| sidecar_id = text(sidecar.get("id"), "unknown") | |
| tone = text(sidecar.get("tone"), "LOW") | |
| self.add_css_class("lead-card" if lead else "sidecar-card") | |
| self.add_css_class(f"sidecar-{sidecar_id}") | |
| if not lead: | |
| self.set_hexpand(True) | |
| self.set_vexpand(True) | |
| header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) | |
| header.set_valign(Gtk.Align.START) | |
| self.append(header) | |
| icon = Gtk.Box() | |
| icon.set_size_request(58 if lead else 48, 58 if lead else 48) | |
| icon.set_halign(Gtk.Align.START) | |
| icon.set_valign(Gtk.Align.START) | |
| icon.add_css_class("sidecar-icon") | |
| icon_image = Gtk.Image.new_from_icon_name(SIDECAR_ICONS.get(sidecar_id, "dialog-information-symbolic")) | |
| icon_image.set_pixel_size(30 if lead else 24) | |
| icon_image.set_halign(Gtk.Align.CENTER) | |
| icon_image.set_valign(Gtk.Align.CENTER) | |
| icon.append(icon_image) | |
| header.append(icon) | |
| title_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) | |
| title_box.set_hexpand(True) | |
| header.append(title_box) | |
| if lead: | |
| eyebrow = Gtk.Label(label="LEAD SIDECAR") | |
| eyebrow.set_xalign(0) | |
| eyebrow.add_css_class("eyebrow") | |
| title_box.append(eyebrow) | |
| title = Gtk.Label(label=text(sidecar.get("title"), "Sidecar")) | |
| title.set_xalign(0) | |
| title.set_wrap(True) | |
| title.add_css_class("lead-title" if lead else "sidecar-title") | |
| title_box.append(title) | |
| badge = Gtk.Label(label=tone) | |
| badge.add_css_class("badge") | |
| badge.add_css_class(f"badge-{tone.lower()}") | |
| header.append(badge) | |
| self.copy = Gtk.Label() | |
| self.copy.set_xalign(0) | |
| self.copy.set_valign(Gtk.Align.START) | |
| self.copy.set_wrap(True) | |
| self.copy.set_wrap_mode(Pango.WrapMode.WORD_CHAR) | |
| self.copy.add_css_class("lead-copy" if lead else "sidecar-copy") | |
| self.append(self.copy) | |
| def update(self, sentences: list[str]) -> None: | |
| self.copy.set_label("\n".join(sentences[:3])) | |
| class CockpitWindow(Adw.ApplicationWindow): | |
| def __init__(self, app: Adw.Application) -> None: | |
| super().__init__(application=app, title="AI Cockpit") | |
| self.set_default_size(1120, 760) | |
| self.store = StateStore() | |
| self.state = self.store.load_state() | |
| self.focus_entry = Gtk.Entry() | |
| self.focus_entry.set_has_frame(False) | |
| self.focus_entry.set_hexpand(True) | |
| self.focus_entry.set_editable(False) | |
| self.focus_entry.add_css_class("focus-entry") | |
| self.focus_subtitle = Gtk.Label() | |
| self.focus_goal_copy = Gtk.Label() | |
| self.sidebar_focus_name = Gtk.Label() | |
| self.active_time_label = Gtk.Label() | |
| self.file_label = Gtk.Label() | |
| self.work_status_label = Gtk.Label() | |
| self.code_label = Gtk.Label() | |
| self.notes_view = Gtk.TextView() | |
| self.threads_view = Gtk.TextView() | |
| self.todo_list = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=9) | |
| self.saved_label = Gtk.Label() | |
| self.language_label = Gtk.Label() | |
| self.lead_slot = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) | |
| self.quiet_grid = Gtk.Grid(column_spacing=14, row_spacing=14) | |
| self.next_copy = Gtk.Label() | |
| self._build() | |
| self.render_state() | |
| def _build(self) -> None: | |
| shell = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) | |
| shell.add_css_class("app-shell") | |
| self.set_content(shell) | |
| shell.append(self._build_sidebar()) | |
| scroller = Gtk.ScrolledWindow() | |
| scroller.set_hexpand(True) | |
| shell.append(scroller) | |
| root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16) | |
| root.add_css_class("main") | |
| scroller.set_child(root) | |
| root.append(self._build_focus_panel()) | |
| body = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=16) | |
| body.set_hexpand(True) | |
| body.set_vexpand(True) | |
| root.append(body) | |
| work_panel = self._build_work_panel() | |
| work_panel.set_size_request(430, -1) | |
| body.append(work_panel) | |
| output = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16) | |
| output.set_hexpand(True) | |
| output.set_vexpand(True) | |
| body.append(output) | |
| self.lead_slot.set_hexpand(True) | |
| output.append(self.lead_slot) | |
| self.quiet_grid.set_column_homogeneous(True) | |
| self.quiet_grid.set_row_homogeneous(True) | |
| self.quiet_grid.set_hexpand(True) | |
| self.quiet_grid.set_vexpand(True) | |
| output.append(self.quiet_grid) | |
| root.append(self._build_next_step()) | |
| hint = Gtk.Label(label="Hinweis: Nur ein Sidecar ist Lead. Die anderen sind leise Karten.") | |
| hint.set_halign(Gtk.Align.CENTER) | |
| hint.add_css_class("hint") | |
| root.append(hint) | |
| def _build_sidebar(self) -> Gtk.Box: | |
| sidebar = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=18) | |
| sidebar.add_css_class("sidebar") | |
| sidebar.set_size_request(214, -1) | |
| brand = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) | |
| sidebar.append(brand) | |
| brand_icon = Gtk.Image.new_from_icon_name("applications-science-symbolic") | |
| brand_icon.set_pixel_size(36) | |
| brand_icon.add_css_class("sidecar-focus") | |
| brand.append(brand_icon) | |
| brand_text = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) | |
| brand.append(brand_text) | |
| title = Gtk.Label(label="AI Cockpit") | |
| title.set_xalign(0) | |
| title.add_css_class("brand-title") | |
| brand_text.append(title) | |
| subtitle = Gtk.Label(label="Dein agentischer Leitstand") | |
| subtitle.set_xalign(0) | |
| subtitle.add_css_class("brand-subtitle") | |
| brand_text.append(subtitle) | |
| nav = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4) | |
| sidebar.append(nav) | |
| for index, (icon_name, label) in enumerate( | |
| ( | |
| ("view-dashboard-symbolic", "Cockpit"), | |
| ("find-location-symbolic", "Fokus"), | |
| ("package-x-generic-symbolic", "Komponenten"), | |
| ("folder-symbolic", "Parks"), | |
| ("text-x-generic-symbolic", "Beweise"), | |
| ("utilities-system-monitor-symbolic", "AI Log"), | |
| ("preferences-system-symbolic", "Einstellungen"), | |
| ) | |
| ): | |
| nav.append(self._nav_item(icon_name, label, active=index == 0)) | |
| spacer = Gtk.Box() | |
| spacer.set_vexpand(True) | |
| sidebar.append(spacer) | |
| focus_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=9) | |
| focus_box.add_css_class("sidebar-focus") | |
| sidebar.append(focus_box) | |
| focus_label = Gtk.Label(label="Aktiver Fokus") | |
| focus_label.set_xalign(0) | |
| focus_label.add_css_class("muted") | |
| focus_box.append(focus_label) | |
| focus_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=7) | |
| focus_box.append(focus_row) | |
| dot = Gtk.Label(label="●") | |
| dot.add_css_class("sidebar-focus-dot") | |
| focus_row.append(dot) | |
| self.sidebar_focus_name.set_xalign(0) | |
| self.sidebar_focus_name.add_css_class("sidebar-focus-name") | |
| focus_row.append(self.sidebar_focus_name) | |
| self.active_time_label.set_xalign(0) | |
| self.active_time_label.add_css_class("muted") | |
| focus_box.append(self.active_time_label) | |
| return sidebar | |
| def _nav_item(self, icon_name: str, label: str, active: bool = False) -> Gtk.Box: | |
| item = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) | |
| item.add_css_class("nav-item") | |
| if active: | |
| item.add_css_class("nav-item-active") | |
| icon = Gtk.Image.new_from_icon_name(icon_name) | |
| icon.set_pixel_size(24) | |
| icon.add_css_class("nav-icon") | |
| item.append(icon) | |
| item_label = Gtk.Label(label=label) | |
| item_label.set_xalign(0) | |
| item.append(item_label) | |
| return item | |
| def _build_focus_panel(self) -> Gtk.Box: | |
| focus_panel = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=22) | |
| focus_panel.add_css_class("focus-panel") | |
| focus_icon = Gtk.Box() | |
| focus_icon.set_size_request(70, 70) | |
| focus_icon.set_halign(Gtk.Align.START) | |
| focus_icon.set_valign(Gtk.Align.CENTER) | |
| focus_icon.add_css_class("sidecar-icon") | |
| focus_icon.add_css_class("sidecar-scope") | |
| focus_image = Gtk.Image.new_from_icon_name("find-location-symbolic") | |
| focus_image.set_pixel_size(38) | |
| focus_image.set_halign(Gtk.Align.CENTER) | |
| focus_image.set_valign(Gtk.Align.CENTER) | |
| focus_icon.append(focus_image) | |
| focus_panel.append(focus_icon) | |
| focus_text = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5) | |
| focus_text.set_hexpand(True) | |
| focus_panel.append(focus_text) | |
| eyebrow = Gtk.Label(label="AKTIVER FOKUS") | |
| eyebrow.set_xalign(0) | |
| eyebrow.add_css_class("eyebrow") | |
| focus_text.append(eyebrow) | |
| focus_text.append(self.focus_entry) | |
| self.focus_subtitle.set_xalign(0) | |
| self.focus_subtitle.add_css_class("focus-subtitle") | |
| focus_text.append(self.focus_subtitle) | |
| target = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8) | |
| target.set_size_request(360, -1) | |
| target.set_valign(Gtk.Align.CENTER) | |
| target.add_css_class("focus-target") | |
| focus_panel.append(target) | |
| target_label = Gtk.Label(label="FOKUS-ZIEL") | |
| target_label.set_xalign(0) | |
| target_label.add_css_class("focus-goal") | |
| target.append(target_label) | |
| self.focus_goal_copy.set_xalign(0) | |
| self.focus_goal_copy.set_wrap(True) | |
| target.append(self.focus_goal_copy) | |
| return focus_panel | |
| def _build_work_panel(self) -> Gtk.Box: | |
| panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) | |
| panel.add_css_class("work-panel") | |
| title = Gtk.Label(label="DEINE AKTUELLE ARBEIT") | |
| title.set_xalign(0) | |
| title.add_css_class("section-title") | |
| panel.append(title) | |
| panel.append(self._build_code_panel()) | |
| panel.append(self._text_field("NOTIZEN", self.notes_view, height=88)) | |
| panel.append(self._text_field("OFFENE FÄDEN", self.threads_view, height=70)) | |
| panel.append(self._build_todo_panel()) | |
| panel.append(self._build_action_bar()) | |
| footer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) | |
| footer.add_css_class("work-footer") | |
| panel.append(footer) | |
| self.saved_label.set_xalign(0) | |
| self.saved_label.set_hexpand(True) | |
| footer.append(self.saved_label) | |
| footer.append(self.language_label) | |
| return panel | |
| def _build_code_panel(self) -> Gtk.Box: | |
| panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) | |
| panel.add_css_class("code-panel") | |
| header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) | |
| header.add_css_class("code-header") | |
| panel.append(header) | |
| icon = Gtk.Image.new_from_icon_name("text-x-python-symbolic") | |
| icon.set_pixel_size(20) | |
| icon.add_css_class("work-icon") | |
| header.append(icon) | |
| self.file_label.set_xalign(0) | |
| self.file_label.set_hexpand(True) | |
| header.append(self.file_label) | |
| self.work_status_label.add_css_class("muted") | |
| header.append(self.work_status_label) | |
| self.code_label.set_xalign(0) | |
| self.code_label.set_wrap(False) | |
| self.code_label.add_css_class("code-text") | |
| panel.append(self.code_label) | |
| return panel | |
| def _build_todo_panel(self) -> Gtk.Box: | |
| panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=9) | |
| panel.add_css_class("work-section") | |
| title = Gtk.Label(label="TODO") | |
| title.set_xalign(0) | |
| title.add_css_class("field-label") | |
| panel.append(title) | |
| panel.append(self.todo_list) | |
| return panel | |
| def _build_action_bar(self) -> Gtk.Box: | |
| panel = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=9) | |
| panel.add_css_class("work-section") | |
| title = Gtk.Label(label="OWNER-AKTIONEN") | |
| title.set_xalign(0) | |
| title.add_css_class("field-label") | |
| panel.append(title) | |
| grid = Gtk.Grid(column_spacing=8, row_spacing=8) | |
| grid.add_css_class("action-grid") | |
| panel.append(grid) | |
| actions = ( | |
| ("Faden parken", "thread_parked"), | |
| ("Abschlussaktion setzen", "finish_action_set"), | |
| ("Todo erledigen", "todo_completed"), | |
| ("Beweis hinzufügen", "evidence_added"), | |
| ) | |
| for index, (label, event_type) in enumerate(actions): | |
| button = Gtk.Button(label=label) | |
| button.add_css_class("owner-button") | |
| button.connect("clicked", lambda _button, kind=event_type: self._handle_owner_action(kind)) | |
| grid.attach(button, index % 2, index // 2, 1, 1) | |
| return panel | |
| def _todo_row(self, item: dict[str, Any]) -> Gtk.Box: | |
| is_done = item.get("status") == "done" | |
| row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=9) | |
| row.add_css_class("todo-row") | |
| check = Gtk.CheckButton() | |
| check.set_active(is_done) | |
| check.set_sensitive(False) | |
| row.append(check) | |
| label = Gtk.Label(label=text(item.get("text"), "Todo")) | |
| label.set_xalign(0) | |
| label.set_hexpand(True) | |
| row.append(label) | |
| state_label = Gtk.Label(label="Erledigt" if is_done else "Offen") | |
| state_label.add_css_class("todo-done" if is_done else "muted") | |
| row.append(state_label) | |
| return row | |
| def _build_next_step(self) -> Gtk.Box: | |
| next_step = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) | |
| next_step.add_css_class("next-step") | |
| next_label = Gtk.Label(label="NÄCHSTER ERLAUBTER SCHRITT") | |
| next_label.set_xalign(0) | |
| next_label.add_css_class("next-label") | |
| next_step.append(next_label) | |
| self.next_copy.set_xalign(0) | |
| self.next_copy.set_wrap(True) | |
| self.next_copy.set_wrap_mode(Pango.WrapMode.WORD_CHAR) | |
| self.next_copy.add_css_class("next-copy") | |
| next_step.append(self.next_copy) | |
| return next_step | |
| def _field(self, label: str, widget: Gtk.Widget) -> Gtk.Box: | |
| box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) | |
| field_label = Gtk.Label(label=label) | |
| field_label.set_xalign(0) | |
| field_label.add_css_class("field-label") | |
| box.append(field_label) | |
| box.append(widget) | |
| return box | |
| def _text_field(self, label: str, view: Gtk.TextView, height: int) -> Gtk.Box: | |
| view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) | |
| view.set_monospace(False) | |
| view.set_editable(False) | |
| view.set_cursor_visible(False) | |
| frame = Gtk.ScrolledWindow() | |
| frame.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) | |
| frame.set_min_content_height(height) | |
| frame.set_child(view) | |
| return self._field(label, frame) | |
| def render_state(self) -> None: | |
| active_focus = self.state.get("active_focus", {}) | |
| current_work = self.state.get("current_work", {}) | |
| self.focus_entry.set_text(focus_name(self.state)) | |
| self.sidebar_focus_name.set_label(focus_name(self.state)) | |
| self.active_time_label.set_label(text(active_focus.get("active_for"))) | |
| self.focus_subtitle.set_label(text(active_focus.get("subtitle"))) | |
| self.focus_goal_copy.set_label(text(active_focus.get("goal"))) | |
| self.file_label.set_label(text(current_work.get("file_path"))) | |
| self.work_status_label.set_label(text(current_work.get("status"))) | |
| self.code_label.set_label("\n".join(str(line) for line in current_work.get("code_lines", []))) | |
| self.saved_label.set_label(text(current_work.get("saved"))) | |
| self.language_label.set_label(text(current_work.get("language"))) | |
| set_text_view(self.notes_view, "\n".join(str(note) for note in current_work.get("notes", []))) | |
| set_text_view(self.threads_view, self._threads_text()) | |
| self._render_todos() | |
| self._render_cards() | |
| self.next_copy.set_label(text(self.state.get("next_allowed_step"))) | |
| def _threads_text(self) -> str: | |
| rows = [] | |
| for thread in self.state.get("open_threads", []): | |
| rows.append( | |
| f"{text(thread.get('title'), 'Thread')} · " | |
| f"{text(thread.get('kind'), 'THREAD')} · " | |
| f"{text(thread.get('status'), 'open')}" | |
| ) | |
| return "\n".join(rows) | |
| def _render_todos(self) -> None: | |
| while child := self.todo_list.get_first_child(): | |
| self.todo_list.remove(child) | |
| for item in self.state.get("todos", []): | |
| self.todo_list.append(self._todo_row(item)) | |
| def _render_cards(self) -> None: | |
| while child := self.lead_slot.get_first_child(): | |
| self.lead_slot.remove(child) | |
| while child := self.quiet_grid.get_first_child(): | |
| self.quiet_grid.remove(child) | |
| sidecars = list(self.state.get("sidecars", [])) | |
| lead = next((sidecar for sidecar in sidecars if sidecar.get("is_lead") is True), sidecars[0]) | |
| lead_card = SidecarCard(lead, lead=True) | |
| lead_card.update([str(sentence) for sentence in lead.get("sentences", [])]) | |
| self.lead_slot.append(lead_card) | |
| quiet_sidecars = [sidecar for sidecar in sidecars if sidecar is not lead] | |
| for index, sidecar in enumerate(quiet_sidecars): | |
| card = SidecarCard(sidecar) | |
| card.update([str(sentence) for sentence in sidecar.get("sentences", [])]) | |
| self.quiet_grid.attach(card, index % 2, index // 2, 1, 1) | |
| def _handle_owner_action(self, event_type: str) -> None: | |
| payload: dict[str, Any] = {"active_focus": focus_name(self.state)} | |
| if event_type == "thread_parked": | |
| payload.update(self._park_thread()) | |
| elif event_type == "finish_action_set": | |
| payload.update(self._set_finish_action()) | |
| elif event_type == "todo_completed": | |
| payload.update(self._complete_todo()) | |
| elif event_type == "evidence_added": | |
| payload.update(self._add_evidence_note()) | |
| self.store.append_event({"type": event_type, "payload": payload}) | |
| self.store.save_state(self.state) | |
| self.render_state() | |
| def _park_thread(self) -> dict[str, Any]: | |
| for thread in self.state.get("open_threads", []): | |
| if thread.get("kind") != "ACTIVE" and thread.get("status") != "parked": | |
| thread["status"] = "parked" | |
| return {"thread_id": thread.get("id"), "result": "parked"} | |
| return {"result": "no_open_thread"} | |
| def _set_finish_action(self) -> dict[str, Any]: | |
| step = "Schließe die kleinste sichtbare Einheit ab: Unit-Test für evaluate_focus." | |
| self.state["next_allowed_step"] = step | |
| return {"next_allowed_step": step} | |
| def _complete_todo(self) -> dict[str, Any]: | |
| for todo in self.state.get("todos", []): | |
| if todo.get("status") != "done": | |
| todo["status"] = "done" | |
| return {"todo_id": todo.get("id"), "result": "done"} | |
| return {"result": "no_open_todo"} | |
| def _add_evidence_note(self) -> dict[str, Any]: | |
| note = "Beweis hinzugefügt: Owner-Aktion protokolliert." | |
| current_work = self.state.setdefault("current_work", {}) | |
| notes = current_work.setdefault("notes", []) | |
| if isinstance(notes, list): | |
| notes.append(note) | |
| return {"evidence": note} | |
| class CockpitApp(Adw.Application): | |
| def __init__(self) -> None: | |
| super().__init__(application_id=APP_ID, flags=Gio.ApplicationFlags.DEFAULT_FLAGS) | |
| self._css_provider = Gtk.CssProvider() | |
| self._css_provider.load_from_data(CSS.encode("utf-8")) | |
| self._css_installed = False | |
| def do_startup(self) -> None: | |
| Adw.Application.do_startup(self) | |
| Adw.StyleManager.get_default().set_color_scheme(Adw.ColorScheme.FORCE_DARK) | |
| def do_activate(self) -> None: | |
| window = self.props.active_window | |
| if window is None: | |
| window = CockpitWindow(self) | |
| if not self._css_installed: | |
| Gtk.StyleContext.add_provider_for_display( | |
| window.get_display(), | |
| self._css_provider, | |
| Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, | |
| ) | |
| self._css_installed = True | |
| window.present() | |
| def main() -> int: | |
| app = CockpitApp() | |
| return app.run(sys.argv) | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |