add plugin settings, per-tile refresh, settings menu, touch-friendly UI

- Plugin settings infrastructure in PluginManager with config/plugin_settings.cfg
  storage, manifest [settings] sections, CRUD API, plugin_setting_changed signal
- Per-tile refresh intervals: 100ms base tick, per-tile update_interval_ms
  metadata, dynamic tween durations clamped to refresh window
- Settings menu (PopupPanel) with General/Plugins/About tabs, plugin
  activation toggles, per-plugin settings cog buttons
- Plugin settings popup (PopupPanel) with dynamic UI from setting definitions
  (SpinBox/CheckBox/LineEdit), auto-save on change
- Modal behavior: exclusive = true on settings windows
- Touch-friendly sizing: enlarged close buttons, cog buttons, controls, spacing
- Red corner close button redesign with rounded corners, hover/pressed states
- Split system_monitor into local_system_monitor (CPU, Memory) and test plugins
- Plugin activation/deactivation with layout preservation
This commit is contained in:
Eric Smith 2026-05-21 12:47:25 -04:00
parent 12b45b2685
commit 57b36798b9
24 changed files with 1065 additions and 183 deletions

View file

@ -0,0 +1,51 @@
extends RefCounted
class_name CpuCollector
var _previous_total: int = 0
var _previous_idle: int = 0
var _has_previous: bool = false
func collect() -> float:
var stats: PackedStringArray = _read_stats()
if stats.is_empty():
return 0.0
var total: int = 0
var idle: int = 0
for i in range(1, stats.size()):
var val: int = stats[i].to_int()
total += val
if i == 4: # idle is the 4th token (index 4)
idle = val
if not _has_previous:
_previous_total = total
_previous_idle = idle
_has_previous = true
return 0.0
var total_delta: int = total - _previous_total
var idle_delta: int = idle - _previous_idle
_previous_total = total
_previous_idle = idle
if total_delta == 0:
return 0.0
return 100.0 * (1.0 - float(idle_delta) / float(total_delta))
func _read_stats() -> PackedStringArray:
var file := FileAccess.open("/proc/stat", FileAccess.READ)
if file == null:
return PackedStringArray()
var line: String = file.get_line()
if not line.begins_with("cpu "):
return PackedStringArray()
return line.split(" ", false)

View file

@ -0,0 +1,71 @@
extends PluginTile
@onready var title_label: Label = %Title
@onready var label: Label = %Label
@onready var vial_fill: ColorRect = %VialFill
var _collector: CpuCollector
var _displayed_fill: float = 0.0
var _fill_tween: Tween
func initialize() -> void:
module_title = "CPU"
_collector = CpuCollector.new()
_setup_shader()
_style()
func refresh(data: Dictionary) -> void:
var usage: float = _collector.collect()
if usage < 0.0:
return
var pct: int = roundi(usage)
label.text = "%d%%" % pct
# Smoothly tween the vial fill — clamp duration so animation never outlasts the refresh interval
var target: float = usage / 100.0
var update_ms: int = data.get("update_interval_ms", 1000)
var duration: float = minf(0.4, update_ms / 2000.0)
if _fill_tween and _fill_tween.is_valid():
_fill_tween.kill()
_fill_tween = create_tween()
_fill_tween.tween_method(_set_fill, _displayed_fill, target, duration).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC)
func _set_fill(value: float) -> void:
_displayed_fill = value
var mat := vial_fill.material as ShaderMaterial
if mat:
mat.set_shader_parameter("fill", value)
func _setup_shader() -> void:
var mat := ShaderMaterial.new()
mat.shader = preload("res://shaders/vial_fill.gdshader")
mat.set_shader_parameter("liquid_color", Color(0.2, 0.5, 0.8, 1.0))
mat.set_shader_parameter("fill", 0.0)
mat.set_shader_parameter("noise_tex", load("res://assets/textures/noise_100.png"))
vial_fill.material = mat
func _style() -> void:
# Transparent root panel — the VialFill shader provides the background
var panel := StyleBoxFlat.new()
panel.bg_color = Color.TRANSPARENT
panel.corner_radius_top_left = 12
panel.corner_radius_top_right = 12
panel.corner_radius_bottom_right = 12
panel.corner_radius_bottom_left = 12
add_theme_stylebox_override("panel", panel)
add_theme_stylebox_override("panel_focused", panel)
title_label.add_theme_color_override("font_color", Color(0.7, 0.7, 0.8, 1.0))
title_label.add_theme_constant_override("outline_size", 2)
title_label.add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 0.5))
label.add_theme_color_override("font_color", Color(0.9, 0.9, 1.0, 1.0))
label.add_theme_font_size_override("font_size", 48)
label.add_theme_constant_override("outline_size", 3)
label.add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 0.7))

View file

@ -0,0 +1,41 @@
[gd_scene format=3 uid="uid://bq3bs2hb4r7fb"]
[ext_resource type="Script" path="res://plugins/local_system_monitor/tiles/cpu/cpu_tile.gd" id="1"]
[ext_resource type="Shader" path="res://shaders/vial_fill.gdshader" id="2"]
[node name="CpuTile" type="PanelContainer"]
anchors_preset = 0
script = ExtResource("1")
[node name="VialFill" type="ColorRect" parent="."]
unique_name_in_owner = true
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 2
[node name="MarginContainer" type="MarginContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
theme_constant_overrides/margin_left = 12
theme_constant_overrides/margin_top = 12
theme_constant_overrides/margin_right = 12
theme_constant_overrides/margin_bottom = 12
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
[node name="Title" type="Label" parent="MarginContainer/VBoxContainer"]
unique_name_in_owner = true
text = "CPU"
[node name="Spacer" type="Control" parent="MarginContainer/VBoxContainer"]
size_flags_vertical = 3
[node name="Label" type="Label" parent="MarginContainer/VBoxContainer"]
unique_name_in_owner = true
horizontal_alignment = 1
size_flags_horizontal = 4

View file

@ -0,0 +1,36 @@
extends RefCounted
class_name MemoryCollector
func collect() -> float:
var meminfo := _read_meminfo()
if meminfo.is_empty():
return 0.0
var total: int = meminfo.get("MemTotal", 0)
var available: int = meminfo.get("MemAvailable", 0)
if total == 0:
return 0.0
return 100.0 * (1.0 - float(available) / float(total))
func _read_meminfo() -> Dictionary:
var file := FileAccess.open("/proc/meminfo", FileAccess.READ)
if file == null:
return {}
var result: Dictionary = {}
while not file.eof_reached():
var line: String = file.get_line().strip_edges()
if line.is_empty():
continue
var parts := line.split(":", false)
if parts.size() < 2:
continue
var key := parts[0].strip_edges()
var val_str := parts[1].strip_edges().split(" ", false)[0]
result[key] = val_str.to_int()
return result

View file

@ -0,0 +1,71 @@
extends PluginTile
@onready var title_label: Label = %Title
@onready var label: Label = %Label
@onready var vial_fill: ColorRect = %VialFill
var _collector: MemoryCollector
var _displayed_fill: float = 0.0
var _fill_tween: Tween
func initialize() -> void:
module_title = "Memory"
_collector = MemoryCollector.new()
_setup_shader()
_style()
func refresh(data: Dictionary) -> void:
var usage: float = _collector.collect()
if usage < 0.0:
return
var pct: int = roundi(usage)
label.text = "%d%%" % pct
# Smoothly tween the vial fill — clamp duration so animation never outlasts the refresh interval
var target: float = usage / 100.0
var update_ms: int = data.get("update_interval_ms", 1000)
var duration: float = minf(0.4, update_ms / 2000.0)
if _fill_tween and _fill_tween.is_valid():
_fill_tween.kill()
_fill_tween = create_tween()
_fill_tween.tween_method(_set_fill, _displayed_fill, target, duration).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CUBIC)
func _set_fill(value: float) -> void:
_displayed_fill = value
var mat := vial_fill.material as ShaderMaterial
if mat:
mat.set_shader_parameter("fill", value)
func _setup_shader() -> void:
var mat := ShaderMaterial.new()
mat.shader = preload("res://shaders/vial_fill.gdshader")
mat.set_shader_parameter("liquid_color", Color(0.2, 0.7, 0.4, 1.0))
mat.set_shader_parameter("fill", 0.0)
mat.set_shader_parameter("noise_tex", load("res://assets/textures/noise_100.png"))
vial_fill.material = mat
func _style() -> void:
# Transparent root panel — the VialFill shader provides the background
var panel := StyleBoxFlat.new()
panel.bg_color = Color.TRANSPARENT
panel.corner_radius_top_left = 12
panel.corner_radius_top_right = 12
panel.corner_radius_bottom_right = 12
panel.corner_radius_bottom_left = 12
add_theme_stylebox_override("panel", panel)
add_theme_stylebox_override("panel_focused", panel)
title_label.add_theme_color_override("font_color", Color(0.7, 0.7, 0.8, 1.0))
title_label.add_theme_constant_override("outline_size", 2)
title_label.add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 0.5))
label.add_theme_color_override("font_color", Color(0.9, 0.9, 1.0, 1.0))
label.add_theme_font_size_override("font_size", 48)
label.add_theme_constant_override("outline_size", 3)
label.add_theme_color_override("font_outline_color", Color(0.0, 0.0, 0.0, 0.7))

View file

@ -0,0 +1,41 @@
[gd_scene format=3 uid="uid://d2d4uqrd2hh3d"]
[ext_resource type="Script" path="res://plugins/local_system_monitor/tiles/memory/memory_tile.gd" id="1"]
[ext_resource type="Shader" path="res://shaders/vial_fill.gdshader" id="2"]
[node name="MemoryTile" type="PanelContainer"]
anchors_preset = 0
script = ExtResource("1")
[node name="VialFill" type="ColorRect" parent="."]
unique_name_in_owner = true
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 2
[node name="MarginContainer" type="MarginContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
theme_constant_overrides/margin_left = 12
theme_constant_overrides/margin_top = 12
theme_constant_overrides/margin_right = 12
theme_constant_overrides/margin_bottom = 12
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
[node name="Title" type="Label" parent="MarginContainer/VBoxContainer"]
unique_name_in_owner = true
text = "Memory"
[node name="Spacer" type="Control" parent="MarginContainer/VBoxContainer"]
size_flags_vertical = 3
[node name="Label" type="Label" parent="MarginContainer/VBoxContainer"]
unique_name_in_owner = true
horizontal_alignment = 1
size_flags_horizontal = 4