Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Environment Variable Editor #159

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
67 changes: 39 additions & 28 deletions src/posting/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from posting.commands import PostingProvider
from posting.config import SETTINGS, Settings
from posting.help_screen import HelpScreen
from posting.environment_screen import EnvironmentScreen
from posting.jump_overlay import JumpOverlay
from posting.jumper import Jumper
from posting.scripts import execute_script, uncache_module, Posting as PostingContext
Expand Down Expand Up @@ -565,7 +566,7 @@ async def action_save_request(self) -> None:
async def action_new_request(self) -> None:
"""Open the new request flow."""
await self.collection_tree.new_request_flow(None)

def watch_current_layout(self, layout: Literal["horizontal", "vertical"]) -> None:
"""Update the current layout of the app to be horizontal or vertical."""
classes = {"horizontal", "vertical"}
Expand Down Expand Up @@ -879,30 +880,30 @@ def __init__(
_jumping: Reactive[bool] = reactive(False, init=False, bindings=True)
"""True if 'jump mode' is currently active, otherwise False."""

@work(exclusive=True, group="environment-watcher")
async def watch_environment_files(self) -> None:
"""Watching files that were passed in as the environment."""
async for changes in awatch(*self.environment_files):
# Reload the variables from the environment files.
load_variables(
self.environment_files,
self.settings.use_host_environment,
avoid_cache=True,
)
# Overlay the session variables on top of the environment variables.
update_variables(self.session_env)

# Notify the app that the environment has changed,
# which will trigger a reload of the variables in the relevant widgets.
# Widgets subscribed to this signal can reload as needed.
# For example, AutoComplete dropdowns will want to reload their
# candidate variables when the environment changes.
self.env_changed_signal.publish(None)
self.notify(
title="Environment changed",
message=f"Reloaded {len(changes)} dotenv files",
timeout=3,
)
# @work(exclusive=True, group="environment-watcher")
# async def watch_environment_files(self) -> None:
# """Watching files that were passed in as the environment."""
# async for changes in awatch(*self.environment_files):
# # Reload the variables from the environment files.
# load_variables(
# self.environment_files,
# self.settings.use_host_environment,
# avoid_cache=True,
# )
# # Overlay the session variables on top of the environment variables.
# update_variables(self.session_env)

# # Notify the app that the environment has changed,
# # which will trigger a reload of the variables in the relevant widgets.
# # Widgets subscribed to this signal can reload as needed.
# # For example, AutoComplete dropdowns will want to reload their
# # candidate variables when the environment changes.
# self.env_changed_signal.publish(None)
# self.notify(
# title="Environment changed",
# message=f"Reloaded {len(changes)} dotenv files",
# timeout=3,
# )

@work(exclusive=True, group="collection-watcher")
async def watch_collection_files(self) -> None:
Expand Down Expand Up @@ -997,8 +998,8 @@ def on_mount(self) -> None:
},
screen=self.screen,
)
if self.settings.watch_env_files:
self.watch_environment_files()
# if self.settings.watch_env_files:
# self.watch_environment_files()

if self.settings.watch_collection_files:
self.watch_collection_files()
Expand Down Expand Up @@ -1105,7 +1106,17 @@ def reset_focus(_) -> None:
self.screen.set_focus(focused)

self.set_focus(None)
await self.push_screen(HelpScreen(widget=focused), callback=reset_focus)
await self.push_screen(HelpScreen(widget=focused), callback=reset_focus)

async def show_environment(self) -> None:
focused = self.focused

def reset_focus(_) -> None:
if focused:
self.screen.set_focus(focused)

self.set_focus(None)
await self.push_screen(EnvironmentScreen(widget=focused), callback=reset_focus)

def exit(
self,
Expand Down
9 changes: 9 additions & 0 deletions src/posting/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ def commands(
True,
),
)

commands_to_show.append(
(
"environment: Show environment variables",
app.show_environment,
"Show the environment variables screen",
True,
)
)

return tuple(commands_to_show)

Expand Down
220 changes: 220 additions & 0 deletions src/posting/environment_screen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
from rich.text import Text
from textual.app import ComposeResult
from textual.binding import Binding
from textual.containers import Vertical, VerticalScroll
from textual.screen import ModalScreen
from textual.widget import Widget
from textual.widgets import Label, Input, Select
from textual.widgets.data_table import RowKey


from posting.widgets.datatable import PostingDataTable
from posting.widgets.key_value import KeyValueEditor, KeyValueInput
from posting.widgets.input import PostingInput
from posting.variables import (
get_variables,
update_variables,
remove_variable,
get_environments,
set_current_environment,
get_current_environment,
)
from posting.help_screen import HelpData


class KeyInput(PostingInput):
help = HelpData(
title="Key Input",
description="""\
An input field for entering environment variable keys.
""",
)


class ValueInput(PostingInput):
help = HelpData(
title="Value Input",
description="""\
An input field for entering environment variable values.
""",
)


class EnvironmentModalHeader(Label):
"""The top help bar"""

DEFAULT_CSS = """
EnvironmentModalHeader {
background: $background-lighten-1;
color: $text-muted;
}
"""


class EnvironmentModalFooter(Label):
"""The bottom help bar"""

DEFAULT_CSS = """
EnvironmentModalFooter {
background: $background-lighten-1;
color: $text-muted;
}
"""


class EnvironmentTable(PostingDataTable):
"""
The environment table.
"""

BINDINGS = [
Binding("backspace", action="remove_row", description="Remove row"),
]

def on_mount(self):
self.fixed_columns = 1
self.show_header = False
self.cursor_type = "row"
self.zebra_stripes = True
self.add_columns(*["Key", "Value"])
self.replace_all_rows(get_variables().items())

def watch_has_focus(self, value: bool) -> None:
self._scroll_cursor_into_view()
return super().watch_has_focus(value)

def add_row(
self,
*cells: str | Text,
height: int | None = 1,
key: str | None = None,
label: str | Text | None = None,
explicit_by_user: bool = True,
) -> RowKey:
update_variables({cells[0]: cells[1]})
return super().add_row(
*cells,
height=height,
key=cells[0],
label=label,
explicit_by_user=explicit_by_user,
)

def remove_row(self, row_key: RowKey | str) -> None:
key = self.get_row(row_key)[0]
remove_variable(key)
return super().remove_row(row_key)


class EnvironmentScreen(ModalScreen[None]):
DEFAULT_CSS = """
EnvironmentScreen {
align: center middle;
& > VerticalScroll {
background: $background;
padding: 1 2;
width: 65%;
height: 80%;
border: wide $background-lighten-2;
border-title-color: $text;
border-title-background: $background;
border-title-style: bold;
}

& DataTable#bindings-table {
width: 1fr;
height: 1fr;
}

& EnvironmentModalHeader {
dock: top;
width: 1fr;
content-align: center middle;
}

#footer-area {
dock: bottom;
height: auto;
margin-top: 1;
& EnvironmentModalFocusNote {
width: 1fr;
content-align: center middle;
color: $text-muted 40%;
}

& EnvironmentModalFooter {
width: 1fr;
content-align: center middle;
}
}


& #bindings-title {
width: 1fr;
content-align: center middle;
background: $background-lighten-1;
color: $text-muted;
}

& #help-description-wrapper {
dock: top;
max-height: 50%;
margin-top: 1;
height: auto;
width: 1fr;
& #help-description {
margin: 0;
width: 1fr;
height: auto;
}
}
}
"""

BINDINGS = [
Binding("escape", "dismiss('')", "Close Environment"),
]

def __init__(
self,
widget: Widget,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
) -> None:
super().__init__(name, id, classes)
self.widget = widget

def on_mount(self) -> None:
self.query_one("#environment-table", EnvironmentTable).focus()

def compose(self) -> ComposeResult:
with VerticalScroll() as vs:
yield EnvironmentModalHeader("[b]Edit Environment Variables[/]")

yield Select(
[(env.name, env) for env in get_environments()],
allow_blank=False,
value=get_current_environment(),
)

yield KeyValueEditor(
EnvironmentTable(id="environment-table"),
KeyValueInput(
KeyInput(placeholder="Key", id="environment-key-input"),
ValueInput(placeholder="Value", id="environment-value-input"),
button_label="Add",
),
empty_message="There are no environment variables.",
)

with Vertical(id="footer-area"):
yield EnvironmentModalFooter("Press [b]ESC[/] to dismiss.")

def on_select_changed(self, changed: Select.Changed) -> None:
set_current_environment(changed.value)
self.query_one("#environment-key-input", Input).value = ""
self.query_one("#environment-value-input", Input).value = ""
env_table = self.query_one("#environment-table", EnvironmentTable)
env_table.replace_all_rows(get_variables().items())
env_table.focus()
Loading