Skip to content

Commit 09d8031

Browse files
committed
tui layout
1 parent 7defc95 commit 09d8031

File tree

9 files changed

+70
-11
lines changed

9 files changed

+70
-11
lines changed

.github/workflows/python-publish.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ jobs:
1818
- name: Build the package
1919
run: python3 -m pip install --upgrade build && python3 -m build
2020
- name: Publish package
21-
uses: pypa/gh-action-pypi-publish@v1.8.10
21+
uses: pypa/gh-action-pypi-publish@release/v1
2222
with:
2323
password: ${{ secrets.PYPI_GITHUB_MININTERFACE }}

docs/Changelog.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Changelog
22

3-
## 0.7.2
3+
## 0.7.3 (2025-01-09)
4+
* fix: put GUI descriptions back to the bottom
5+
6+
## 0.7.2 (2024-12-30)
47
* GUI calendar
58

69
## 0.7.1 (2024-11-27)

mininterface/textual_interface/textual_adaptor.py

+5
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ class TextualAdaptor(BackendAdaptor):
2828
def __init__(self, interface: "TextualInterface"):
2929
self.interface = interface
3030
self.facet = interface.facet = TextualFacet(self, interface.env)
31+
self.app: TextualApp | None = None
32+
self.layout_elements = []
3133

3234
@staticmethod
3335
def widgetize(tag: Tag) -> Widget | Changeable:
@@ -73,6 +75,9 @@ def header(self, text: str):
7375

7476
def run_dialog(self, form: TagDict, title: str = "", submit: bool | str = True) -> TagDict:
7577
super().run_dialog(form, title, submit)
78+
# Unfortunately, there seems to be no way to reuse the app.
79+
# Which blocks using multiple form external .form() calls from the web interface.
80+
# Textual cannot run in a thread, it seems it cannot run in another process, self.suspend() is of no use.
7681
self.app = app = TextualApp(self, submit)
7782
if title:
7883
app.title = title

mininterface/textual_interface/textual_app.py

+9
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ class TextualApp(App[bool | None]):
2626
# ("down", "go_up", "Go down"),
2727
# ]
2828

29+
DEFAULT_CSS = """
30+
ImageViewer{
31+
32+
height: 20;
33+
}
34+
"""
35+
""" Limit layout image size """
36+
2937
def __init__(self, adaptor: "TextualAdaptor", submit: str | bool = True):
3038
super().__init__()
3139
self.title = adaptor.facet._title
@@ -52,6 +60,7 @@ def compose(self) -> ComposeResult:
5260
yield Label(text, id="buffered_text")
5361
focus_set = False
5462
with VerticalScroll():
63+
yield from self.adaptor.layout_elements
5564
for i, fieldt in enumerate(self.widgets):
5665
if isinstance(fieldt, Input):
5766
yield Label(fieldt.placeholder)

mininterface/textual_interface/textual_button_app.py

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import TYPE_CHECKING, Any
33

44
from textual.app import App, ComposeResult
5+
from textual.containers import VerticalScroll
56
from textual.widgets import Button, Footer, Label
67

78
from ..exceptions import Cancelled
@@ -60,6 +61,7 @@ def __init__(self, interface: "TextualInterface"):
6061
self.focused_i: int = 0
6162
self.values = {}
6263
self.interface = interface
64+
self.adaptor = self.interface.adaptor
6365

6466
def yes_no(self, text: str, focus_no=True) -> DummyWrapper:
6567
return self.buttons(text, [("Yes", True), ("No", False)], int(focus_no))
@@ -78,6 +80,7 @@ def compose(self) -> ComposeResult:
7880
yield Footer()
7981
if text := self.interface._redirected.join():
8082
yield Label(text, id="buffered_text")
83+
yield from self.adaptor.layout_elements
8184
yield Label(self.text, id="question")
8285

8386
self.values.clear()

mininterface/textual_interface/textual_facet.py

+34-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1+
from datetime import datetime
12
from typing import TYPE_CHECKING
23
from warnings import warn
3-
from ..facet import Facet
4+
from pathlib import Path
5+
6+
from textual.widgets import (Checkbox, Footer, Header, Input, Label,
7+
RadioButton, Static)
8+
9+
from humanize import naturalsize
10+
11+
from ..exceptions import DependencyRequired
12+
from ..facet import Facet, Image, LayoutElement
413
if TYPE_CHECKING:
514
from .textual_adaptor import TextualAdaptor
615

@@ -22,6 +31,30 @@ def set_title(self, title: str):
2231
# NOTE: When you receive Facet in Command.init, the app does not exist yet
2332
warn("Setting textual title not implemented well.")
2433

34+
def _layout(self, elements: list[LayoutElement]):
35+
append = self.adaptor.layout_elements.append
36+
try:
37+
from PIL import Image as ImagePIL
38+
from textual_imageview.viewer import ImageViewer
39+
PIL = True
40+
except:
41+
PIL = False
42+
43+
for el in elements:
44+
match el:
45+
case Image():
46+
if not PIL:
47+
raise DependencyRequired("img")
48+
append(ImageViewer(ImagePIL.open(el.src)))
49+
case Path():
50+
size = naturalsize(el.stat().st_size)
51+
mtime = datetime.fromtimestamp(el.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S")
52+
append(Label(f"{el} / {size} / {mtime}"))
53+
case str():
54+
append(Label(el))
55+
case _:
56+
append(Label("Error in the layout: Unknown {el}"))
57+
2558
def submit(self, *args, **kwargs):
2659
super().submit(*args, **kwargs)
2760
try:

mininterface/tk_interface/external_fix.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
# The purpose of the file is to put the descriptions to the bottom of the widgets as it was in the former version of the tkinter_form.
1+
# The purpose of the file is to put the descriptions to the bottom of the widgets
2+
# as it was in the former version of the tkinter_form and to limit their width.
23
from tkinter import ttk
34

45
from tkinter_form import Form, Value, FieldForm
@@ -48,7 +49,7 @@ def __create_widgets_monkeypatched(
4849
description_label = None
4950
if not description is None:
5051
index += 1
51-
description_label = ttk.Label(self, text=description)
52+
description_label = ttk.Label(self, text=description, wraplength=1000)
5253
description_label.grid(row=index, column=1, columnspan=2, sticky="nesw", padx=2, pady=2)
5354

5455
self.fields[name_key] = FieldForm(

mininterface/tk_interface/tk_facet.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@ def _layout(self, elements: list[LayoutElement]):
3636
raise DependencyRequired("img")
3737
filename = el.src
3838
img = ImagePIL.open(filename)
39-
img = img.resize((250, 250))
40-
img = ImageTk.PhotoImage(img)
41-
panel = Label(self.adaptor.frame, image=img)
42-
panel.image = img
39+
max_width, max_height = 250, 250
40+
w_o, h_o = img.size
41+
scale = min(max_width / w_o, max_height / h_o)
42+
img = img.resize((int(w_o * scale), int(h_o * scale)), ImagePIL.LANCZOS)
43+
img_p = ImageTk.PhotoImage(img)
44+
panel = Label(self.adaptor.frame, image=img_p)
45+
panel.image = img_p
4346
panel.pack()
4447
case Path():
4548
size = naturalsize(el.stat().st_size)

pyproject.toml

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry]
66
name = "mininterface"
7-
version = "0.7.2"
7+
version = "0.7.3"
88
description = "A minimal access to GUI, TUI, CLI and config"
99
authors = ["Edvard Rejthar <[email protected]>"]
1010
license = "GPL-3.0-or-later"
@@ -27,8 +27,10 @@ tkscrollableframe = "*"
2727

2828
[tool.poetry.extras]
2929
web = ["textual-serve"]
30+
img = ["pillow", "textual_imageview"]
31+
tui = ["textual_imageview"]
3032
gui = ["pillow", "tkcalendar"]
31-
all = ["textual-serve", "pillow", "tkcalendar"]
33+
all = ["textual-serve", "pillow", "tkcalendar", "textual_imageview"]
3234

3335
[tool.poetry.scripts]
3436
mininterface = "mininterface.__main__:main"

0 commit comments

Comments
 (0)