Skip to content

Commit

Permalink
feat: added support for workspace switching (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
idoavrah authored Apr 6, 2024
1 parent 970bd17 commit c14bcd3
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 43 deletions.
18 changes: 6 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ With its latest version you can easily visualize the complete state tree, gainin

## Changelog (latest versions)

### Version 0.13

- [x] Added support for workspace switching
- [x] Added plan summary in the screen title
- [x] Empty tree is now shown when no state exists instead of program shutting down, allowing for plan creation

### Version 0.12

- [x] Enabled targeting specific resources for plan creation
Expand All @@ -40,18 +46,6 @@ With its latest version you can easily visualize the complete state tree, gainin
- [x] Added coloring to tainted resources considering some terminals can't display strikethrough correctly
- [x] Improved loading screen mechanism

### Version 0.10

- [x] Perform actions on a single highlighted resource without pre-selecting it
- [x] User interface overhaul: added logo, fixed coloring and loading indicator
- [x] Added resource selection via the space key and tree traversal via the arrow keys
- [x] Added a lightmode command-line argument
- [x] Added a debug log command-line argument
- [x] Copy to clipboard now copies the resource name in the tree view
- [x] Fixed a bug in the remove resource functionality
- [x] Fixed a bug in the parsing mechanism (colons, dots)
- [x] Refactor: globals

## Demo

![](demo/tftui.gif "demo")
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "tftui"
version = "0.12.6"
version = "0.13.0"
description = "Terraform Textual User Interface"
authors = ["Ido Avraham"]
license = "Apache-2.0"
Expand Down
94 changes: 83 additions & 11 deletions tftui/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@
import json
import traceback
import re
import subprocess
from rich.text import Text
from shutil import which
from tftui.apis import OutboundAPIs
from tftui.state import State, Block, execute_async, split_resource_name
from tftui.plan import PlanScreen
from tftui.debug_log import setup_logging
from tftui.modal import HelpModal, YesNoModal, PlanInputsModal, FullTextModal
from tftui.modal import (
HelpModal,
YesNoModal,
PlanInputsModal,
FullTextModal,
WorkspaceModal,
)
from textual import work
from textual.app import App, Binding
from textual.containers import Horizontal
Expand Down Expand Up @@ -44,22 +51,39 @@ class AppHeader(Horizontal):
\/_/ \/_/ \/_/ \/_____/ \/_/
"""

TITLES = """
TFTUI Version:\n
TITLES = """TFTUI Version:\n
Working folder:\n
"""

INFO = f"""
{OutboundAPIs.version}{' (new version available)' if OutboundAPIs.is_new_version_available else ''}\n
{os.getcwd()}\n
Workspace:
"""

BORDER_TITLE = "TFTUI - the Terraform terminal user interface"

info = Static("", classes="header-box")

def refresh_info(self):
result = subprocess.run(
[ApplicationGlobals.executable, "workspace", "show"],
capture_output=True,
text=True,
)

if result.returncode == 0:
workspace = result.stdout
else:
logger.error(f"Error getting workspace: {result.stderr}")
workspace = "Unknown"

self.info.update(
f"""{OutboundAPIs.version}{' (new version available)' if OutboundAPIs.is_new_version_available else ''}\n
{os.getcwd()}\n
{workspace}"""
)

def compose(self):
yield Static(AppHeader.TITLES, classes="header-box")
yield Static(AppHeader.INFO, classes="header-box")
yield Static(AppHeader.LOGO, classes="header-box")
self.refresh_info()
yield Static(self.TITLES, classes="header-box")
yield self.info
yield Static(self.LOGO, classes="header-box")


class StateTree(Tree):
Expand Down Expand Up @@ -235,6 +259,7 @@ def __init__(self, *args, **kwargs):
("ctrl+d", "destroy", "Destroy"),
("/", "search", "Search"),
("0-9", "collapse", "Collapse"),
("w", "workspaces", "Workspaces"),
("m", "toggle_dark", "Dark mode"),
("h", "help", "Help"),
("q", "quit", "Quit"),
Expand Down Expand Up @@ -512,6 +537,53 @@ def action_search(self) -> None:
self.switcher.current = "tree"
self.search.focus()

def action_workspaces(self) -> None:
if self.switcher.current != "tree":
return

result = subprocess.run(
[ApplicationGlobals.executable, "workspace", "list"],
capture_output=True,
text=True,
)

if result.returncode == 0:
workspaces = []
for workspace in result.stdout.split("\n"):
if workspace.strip():
workspaces.append(workspace[2:])
if workspace.startswith("*"):
current_workspace = workspace[2:]
else:
logger.error(f"Error getting workspaces: {result.stderr}")
self.notify("Failed getting workspaces", severity="error")

def switch_workspace(selected_workspace: str):
if (
selected_workspace is not None
and selected_workspace != current_workspace
):
result = subprocess.run(
[
ApplicationGlobals.executable,
"workspace",
"select",
selected_workspace,
],
capture_output=True,
text=True,
)
if result.returncode == 0:
self.app.get_child_by_id("header").refresh_info()
self.action_refresh()
else:
logger.error(f"Failed switching workspaces: {result.stderr}")
self.notify("Failed switching workspaces", severity="error")

self.push_screen(
WorkspaceModal(workspaces, current_workspace), switch_workspace
)

def action_fullscreen(self) -> None:
if self.switcher.current not in ("plan", "resource"):
return
Expand Down
44 changes: 42 additions & 2 deletions tftui/modal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,47 @@
from textual.app import ComposeResult
from textual.containers import Grid
from textual.screen import ModalScreen
from textual.widgets import Button, RichLog, Input, Checkbox, Static, DataTable
from textual.containers import Horizontal
from textual.widgets import (
Button,
RichLog,
Input,
Checkbox,
Static,
DataTable,
OptionList,
)
from textual.containers import Horizontal, Vertical


class WorkspaceModal(ModalScreen):
workspaces = []
current = ""
options = None

def __init__(self, workspaces: list, current: str, *args, **kwargs):
self.current = current
self.workspaces = workspaces
super().__init__(*args, **kwargs)

def compose(self) -> ComposeResult:
question = Static(
Text("Select workspace to switch to:\n", "bold"),
id="question",
)
self.options = OptionList(*self.workspaces)
self.options.highlighted = self.workspaces.index(self.current)
yield Vertical(
question,
self.options,
Button("OK", id="ok"),
id="workspaces",
)

def on_key(self, event) -> None:
if event.key == "enter":
self.dismiss(self.workspaces[self.options.highlighted])
elif event.key == "escape":
self.dismiss(None)


class FullTextModal(ModalScreen):
Expand Down Expand Up @@ -132,6 +171,7 @@ class HelpModal(ModalScreen):
("A", "Apply current plan, available only if a valid plan was created"),
("/", "Filter tree based on text inside resources names and descriptions"),
("0-9", "Collapse the state tree to the selected level, 0 expands all nodes"),
("W", "Switch workspace"),
("M", "Toggle dark mode"),
("Q", "Quit"),
)
Expand Down
4 changes: 4 additions & 0 deletions tftui/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ async def create_plan(self, varfile, targets, destroy="") -> None:
self.auto_scroll = False
self.parent.loading = True
self.fulltext = None
self.app.switcher.border_title = ""
self.clear()
command = [
self.executable,
Expand Down Expand Up @@ -123,6 +124,9 @@ async def create_plan(self, varfile, targets, destroy="") -> None:
if proc.returncode != 2:
self.active_plan = None

if self.active_plan:
self.app.switcher.border_title = self.active_plan.plain.split("\n")[0]

self.focus()

@work(exclusive=True)
Expand Down
5 changes: 1 addition & 4 deletions tftui/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,13 @@ def parse_block(line: str) -> tuple[str, str, str]:

async def refresh_state(self) -> None:
returncode, stdout = await execute_async(self.executable, "show -no-color")
if returncode != 0 or stdout.startswith("No state"):
if returncode != 0:
raise Exception(stdout)

self.state_tree = {}
state_output = stdout.splitlines()
logger.debug(f"state show line count: {len(state_output)}")

if stdout.startswith("The state file is empty."):
return

contents = ""
for line in state_output:
if line.startswith("#"):
Expand Down
38 changes: 25 additions & 13 deletions tftui/ui.tcss
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
ModalScreen {
align: center middle;
}

#header {
border: double lightblue;
border-title-align: center;
Expand Down Expand Up @@ -55,24 +59,16 @@ Button {
width: 100%;
}

HelpModal {
align: center middle;
}

#help {
grid-gutter: 1 2;
grid-rows: 1fr 3;
padding: 0 1;
width: 88;
height: 23;
height: 24;
border: thick $background 80%;
background: $surface;
}

YesNoModal {
align: center middle;
}

#question {
column-span: 2;
border: none;
Expand All @@ -89,10 +85,6 @@ YesNoModal {
background: $surface;
}

PlanInputsModal {
align: center middle;
}

#varfilelabel {
height: 3;
width: 14;
Expand Down Expand Up @@ -123,3 +115,23 @@ PlanInputsModal {
background: $surface;
height: 20;
}

OptionList {
grid-gutter: 1 2;
height: 8;
margin-bottom: 1;
}

Vertical {
align: center middle;
column-span: 1;
}

#workspaces {
grid-size: 2;
padding: 1 2;
width: 30%;
max-height: 18;
border: thick $background 80%;
background: $surface;
}

0 comments on commit c14bcd3

Please sign in to comment.