isobar-cli 1.3.2__tar.gz → 1.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {isobar_cli-1.3.2/src/isobar_cli.egg-info → isobar_cli-1.3.3}/PKG-INFO +1 -1
  2. {isobar_cli-1.3.2 → isobar_cli-1.3.3}/pyproject.toml +1 -1
  3. isobar_cli-1.3.3/src/isobar_cli/__init__.py +1 -0
  4. {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli/logic.py +124 -0
  5. {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli/ui.py +123 -240
  6. {isobar_cli-1.3.2 → isobar_cli-1.3.3/src/isobar_cli.egg-info}/PKG-INFO +1 -1
  7. {isobar_cli-1.3.2 → isobar_cli-1.3.3}/tests/test_isobar_extra.py +4 -4
  8. {isobar_cli-1.3.2 → isobar_cli-1.3.3}/tests/test_ui.py +1 -1
  9. isobar_cli-1.3.2/src/isobar_cli/__init__.py +0 -1
  10. {isobar_cli-1.3.2 → isobar_cli-1.3.3}/LICENSE +0 -0
  11. {isobar_cli-1.3.2 → isobar_cli-1.3.3}/README.md +0 -0
  12. {isobar_cli-1.3.2 → isobar_cli-1.3.3}/setup.cfg +0 -0
  13. {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli/api.py +0 -0
  14. {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli/config.py +0 -0
  15. {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli/location.py +0 -0
  16. {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli/main.py +0 -0
  17. {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli/models.py +0 -0
  18. {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli.egg-info/SOURCES.txt +0 -0
  19. {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli.egg-info/dependency_links.txt +0 -0
  20. {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli.egg-info/entry_points.txt +0 -0
  21. {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli.egg-info/requires.txt +0 -0
  22. {isobar_cli-1.3.2 → isobar_cli-1.3.3}/src/isobar_cli.egg-info/top_level.txt +0 -0
  23. {isobar_cli-1.3.2 → isobar_cli-1.3.3}/tests/test_api.py +0 -0
  24. {isobar_cli-1.3.2 → isobar_cli-1.3.3}/tests/test_location.py +0 -0
  25. {isobar_cli-1.3.2 → isobar_cli-1.3.3}/tests/test_main.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: isobar-cli
3
- Version: 1.3.2
3
+ Version: 1.3.3
4
4
  Summary: A terminal weather tool with industrial aesthetic, focusing on Real Feel and Windchill.
5
5
  Author: Beau Bremer / KnowOneActual
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "isobar-cli"
7
- version = "1.3.2"
7
+ version = "1.3.3"
8
8
  description = "A terminal weather tool with industrial aesthetic, focusing on Real Feel and Windchill."
9
9
  authors = [
10
10
  { name="Beau Bremer / KnowOneActual" },
@@ -0,0 +1 @@
1
+ __version__ = "1.3.3"
@@ -308,3 +308,127 @@ def get_temporal_context(
308
308
  return f"{direction} {abs_diff:.1f}°C {'warmer' if diff > 0 else 'cooler'} than yesterday"
309
309
  else:
310
310
  return f"{direction} {abs_diff:.1f}°F {'warmer' if diff > 0 else 'cooler'} than yesterday"
311
+
312
+
313
+ def get_temperature_comfort(temp: float, unit: str = "°F") -> tuple[str, str]:
314
+ """Get temperature comfort category and color.
315
+
316
+ Args:
317
+ temp: Temperature value
318
+ unit: Temperature unit (°F or °C)
319
+
320
+ Returns:
321
+ Tuple of (category, color) where category is a comfort level
322
+ """
323
+ if unit == "°F":
324
+ if temp < 32:
325
+ return "FREEZING", "bright_cyan"
326
+ elif temp < 50:
327
+ return "COLD", "cyan"
328
+ elif temp < 75:
329
+ return "COMFORTABLE", "bright_green"
330
+ elif temp < 90:
331
+ return "WARM", "bright_yellow"
332
+ else:
333
+ return "HOT", "bright_red"
334
+ else: # °C
335
+ if temp < 0:
336
+ return "FREEZING", "bright_cyan"
337
+ elif temp < 10:
338
+ return "COLD", "cyan"
339
+ elif temp < 24:
340
+ return "COMFORTABLE", "bright_green"
341
+ elif temp < 32:
342
+ return "WARM", "bright_yellow"
343
+ else:
344
+ return "HOT", "bright_red"
345
+
346
+
347
+ def get_humidity_category(humidity: int) -> tuple[str, str]:
348
+ """Get humidity category and color.
349
+
350
+ Args:
351
+ humidity: Humidity percentage (0-100)
352
+
353
+ Returns:
354
+ Tuple of (category, color)
355
+ """
356
+ if humidity < 30:
357
+ return "DRY", "bright_yellow"
358
+ elif humidity < 60:
359
+ return "IDEAL", "bright_green"
360
+ elif humidity < 80:
361
+ return "HUMID", "bright_yellow"
362
+ else:
363
+ return "MUGGY", "bright_red"
364
+
365
+
366
+ def get_wind_category_label(speed: float, unit: str) -> tuple[str, str]:
367
+ """Get wind category label and color.
368
+
369
+ Args:
370
+ speed: Wind speed
371
+ unit: Wind unit (mph or km/h)
372
+
373
+ Returns:
374
+ Tuple of (category, color)
375
+ """
376
+ if unit == "mph":
377
+ if speed < 1:
378
+ return "CALM", "bright_cyan"
379
+ elif speed < 7:
380
+ return "LIGHT", "cyan"
381
+ elif speed < 12:
382
+ return "GENTLE", "bright_green"
383
+ elif speed < 18:
384
+ return "MODERATE", "bright_yellow"
385
+ elif speed < 24:
386
+ return "FRESH", "yellow"
387
+ elif speed < 31:
388
+ return "STRONG", "bright_red"
389
+ elif speed < 38:
390
+ return "GALE", "red"
391
+ elif speed < 46:
392
+ return "SEVERE", "bright_red"
393
+ else:
394
+ return "STORM", "bright_red"
395
+ else: # km/h
396
+ if speed < 2:
397
+ return "CALM", "bright_cyan"
398
+ elif speed < 12:
399
+ return "LIGHT", "cyan"
400
+ elif speed < 20:
401
+ return "GENTLE", "bright_green"
402
+ elif speed < 29:
403
+ return "MODERATE", "bright_yellow"
404
+ elif speed < 39:
405
+ return "FRESH", "yellow"
406
+ elif speed < 50:
407
+ return "STRONG", "bright_red"
408
+ elif speed < 62:
409
+ return "GALE", "red"
410
+ elif speed < 75:
411
+ return "SEVERE", "bright_red"
412
+ else:
413
+ return "STORM", "bright_red"
414
+
415
+
416
+ def get_uv_category(uv_index: float) -> tuple[str, str]:
417
+ """Get UV index category and color.
418
+
419
+ Args:
420
+ uv_index: UV index value
421
+
422
+ Returns:
423
+ Tuple of (category, color)
424
+ """
425
+ if uv_index <= 2:
426
+ return "LOW", "bright_green"
427
+ elif uv_index <= 5:
428
+ return "MODERATE", "bright_yellow"
429
+ elif uv_index <= 7:
430
+ return "HIGH", "bright_red"
431
+ elif uv_index <= 10:
432
+ return "VERY HIGH", "red"
433
+ else:
434
+ return "EXTREME", "bright_red"
@@ -1,6 +1,5 @@
1
1
  import time
2
2
 
3
- from rich import box
4
3
  from rich.columns import Columns
5
4
  from rich.console import Console
6
5
  from rich.table import Table
@@ -9,11 +8,13 @@ from .logic import (
9
8
  WMO_CODES,
10
9
  get_aqi_label,
11
10
  get_feels_like_label,
12
- get_precip_headline,
11
+ get_humidity_category,
13
12
  get_preparation_guidance,
14
13
  get_temp_color,
14
+ get_temperature_comfort,
15
15
  get_temporal_context,
16
- get_uv_guidance,
16
+ get_uv_category,
17
+ get_wind_category_label,
17
18
  get_wind_gust_alert,
18
19
  )
19
20
  from .models import WeatherData
@@ -34,15 +35,6 @@ COLORS = {
34
35
  "concrete": "white",
35
36
  }
36
37
 
37
- # Weather severity indicators
38
- SEVERITY_ICONS = {
39
- "extreme": "⚡",
40
- "high": "▲",
41
- "medium": "●",
42
- "low": "○",
43
- "normal": "◇",
44
- }
45
-
46
38
 
47
39
  def get_weather_icon(code: int) -> tuple[str, str]:
48
40
  """Return (emoji, description) for a WMO weather code."""
@@ -50,7 +42,7 @@ def get_weather_icon(code: int) -> tuple[str, str]:
50
42
 
51
43
 
52
44
  def build_weather_table(weather: WeatherData) -> Table:
53
- """Constructs the current conditions weather table with industrial aesthetic."""
45
+ """Constructs the current conditions weather table with borderless industrial aesthetic."""
54
46
  icon, desc = get_weather_icon(weather.weather_code)
55
47
  temp_color = get_temp_color(weather.temp, weather.units.temp)
56
48
  feels_color = get_temp_color(weather.feels_like, weather.units.temp)
@@ -58,47 +50,50 @@ def build_weather_table(weather: WeatherData) -> Table:
58
50
  weather.temp, weather.feels_like, weather.units.temp
59
51
  )
60
52
 
61
- # Create industrial-style table with heavy borders
53
+ # Get meaningful labels for weather data
54
+ temp_comfort, temp_comfort_color = get_temperature_comfort(
55
+ weather.temp, weather.units.temp
56
+ )
57
+ humidity_category, humidity_color = get_humidity_category(weather.humidity)
58
+ wind_category, wind_color = get_wind_category_label(
59
+ weather.wind_speed, weather.units.wind
60
+ )
61
+
62
+ # Create borderless industrial-style table
62
63
  table = Table(
63
- title=f"[{COLORS['primary']} bold]┌─ WEATHER OBSERVATORY ─┐[/{COLORS['primary']} bold]\n"
64
- f"[{COLORS['steel']}]{weather.city.upper()}[/{COLORS['steel']}]",
64
+ title=f"[{COLORS['primary']} bold]WEATHER OBSERVATORY · {weather.city.upper()}[/{COLORS['primary']} bold]",
65
65
  show_header=False,
66
- box=box.HEAVY,
67
- border_style=COLORS["secondary"],
68
- title_style=f"{COLORS['primary']} bold",
69
- padding=(0, 2),
66
+ box=None, # No borders
67
+ padding=(0, 1),
70
68
  width=60,
69
+ title_style=f"{COLORS['primary']} bold",
71
70
  )
72
71
 
73
- # Add columns with industrial styling
74
- table.add_column("", justify="center", width=4, style=COLORS["accent"])
75
- table.add_column("METRIC", justify="left", width=20, style=COLORS["muted"])
76
- table.add_column("READING", justify="right", width=15, style=COLORS["primary"])
77
- table.add_column("STATUS", justify="right", width=15, style=COLORS["secondary"])
78
-
79
- # Weather condition row with severity indicator
80
- severity = (
81
- "medium" if weather.weather_code in [95, 96, 99, 65, 75, 86] else "normal"
72
+ # Add columns: icon, metric, value, category
73
+ table.add_column("", justify="center", width=3, style=COLORS["accent"])
74
+ table.add_column(
75
+ "METRIC", justify="left", width=18, style=f"{COLORS['muted']} bold"
82
76
  )
77
+ table.add_column("VALUE", justify="right", width=12, style=COLORS["primary"])
78
+ table.add_column("CATEGORY", justify="right", width=20, style=COLORS["secondary"])
79
+
80
+ # Weather condition
83
81
  table.add_row(
84
82
  f"[{COLORS['accent']}]{icon}[/{COLORS['accent']}]",
85
- "[bold]CONDITIONS[/bold]",
86
- f"[{COLORS['primary']} bold]{desc.upper()}[/{COLORS['primary']} bold]",
87
- f"[{COLORS['warning']}]{SEVERITY_ICONS[severity]}[/{COLORS['warning']}]",
83
+ "CONDITIONS",
84
+ f"[{COLORS['primary']}]{desc.upper()}[/{COLORS['primary']}]",
85
+ "",
88
86
  )
89
87
 
90
- # Temperature with industrial gauge visualization
91
- temp_gauge = create_gauge(
92
- weather.temp, weather.units.temp, 100 if weather.units.temp == "°F" else 40
93
- )
88
+ # Temperature with comfort category
94
89
  table.add_row(
95
90
  "🌡️",
96
- "[bold]TEMPERATURE[/bold]",
91
+ "TEMPERATURE",
97
92
  f"[{temp_color}]{weather.temp}{weather.units.temp}[/{temp_color}]",
98
- temp_gauge,
93
+ f"[{temp_comfort_color}]{temp_comfort}[/{temp_comfort_color}]",
99
94
  )
100
95
 
101
- # Feels like with difference indicator
96
+ # Feels like with difference
102
97
  temp_diff = weather.feels_like - weather.temp
103
98
  diff_symbol = "▲" if temp_diff > 0 else "▼" if temp_diff < 0 else "▬"
104
99
  diff_color = (
@@ -108,195 +103,115 @@ def build_weather_table(weather: WeatherData) -> Table:
108
103
  if temp_diff < -5
109
104
  else COLORS["muted"]
110
105
  )
106
+ feels_comfort, feels_comfort_color = get_temperature_comfort(
107
+ weather.feels_like, weather.units.temp
108
+ )
111
109
  table.add_row(
112
110
  "🤔",
113
- f"[bold]{feels_label.upper()}[/bold]",
111
+ f"{feels_label.upper()}",
114
112
  f"[{feels_color}]{weather.feels_like}{weather.units.temp}[/{feels_color}]",
115
- f"[{diff_color}]{diff_symbol} {abs(temp_diff):.1f}{weather.units.temp}[/{diff_color}]",
113
+ f"[{feels_comfort_color}]{feels_comfort} [{diff_color}]{diff_symbol}{abs(temp_diff):.1f}{weather.units.temp}[/{diff_color}][/{feels_comfort_color}]",
116
114
  )
117
115
 
118
- # Wind with speed category
119
- wind_category = get_wind_category(weather.wind_speed, weather.units.wind)
116
+ # Wind with category
120
117
  table.add_row(
121
118
  "💨",
122
- "[bold]WIND SPEED[/bold]",
119
+ "WIND SPEED",
123
120
  f"{weather.wind_speed} {weather.units.wind}",
124
- f"[{COLORS['steel']}]{wind_category}[/{COLORS['steel']}]",
121
+ f"[{wind_color}]{wind_category}[/{wind_color}]",
125
122
  )
126
123
 
127
- # Humidity with moisture indicator
128
- humidity_indicator = "▓" * (weather.humidity // 20) + "░" * (
129
- 5 - weather.humidity // 20
130
- )
124
+ # Humidity with category
131
125
  table.add_row(
132
126
  "💧",
133
- "[bold]HUMIDITY[/bold]",
127
+ "HUMIDITY",
134
128
  f"{weather.humidity}%",
135
- f"[{COLORS['accent']}]{humidity_indicator}[/{COLORS['accent']}]",
129
+ f"[{humidity_color}]{humidity_category}[/{humidity_color}]",
136
130
  )
137
131
 
138
- # Air Quality with health impact
132
+ # Air Quality
139
133
  if weather.aqi is not None:
140
134
  label, color = get_aqi_label(weather.aqi)
141
- aqi_severity = (
142
- "high" if weather.aqi > 100 else "medium" if weather.aqi > 50 else "low"
143
- )
144
135
  table.add_row(
145
136
  "😷",
146
- "[bold]AIR QUALITY[/bold]",
137
+ "AIR QUALITY",
147
138
  f"[{color}]{weather.aqi}[/{color}]",
148
- f"[{color}]{label.upper()} {SEVERITY_ICONS[aqi_severity]}[/{color}]",
139
+ f"[{color}]{label.upper()}[/{color}]",
149
140
  )
150
141
 
151
- # Precipitation with visual indicator
152
- precip_indicator = "●" * min(weather.precip_prob // 20, 5)
153
- headline = get_precip_headline(
154
- weather.precip_prob, weather.rainfall, weather.snowfall, weather.units.precip
142
+ # Precipitation
143
+ precip_category = (
144
+ "DRY"
145
+ if weather.precip_prob < 20
146
+ else "LIGHT"
147
+ if weather.precip_prob < 50
148
+ else "MODERATE"
149
+ if weather.precip_prob < 80
150
+ else "HEAVY"
151
+ )
152
+ precip_color = (
153
+ COLORS["success"]
154
+ if weather.precip_prob < 20
155
+ else COLORS["warning"]
156
+ if weather.precip_prob < 50
157
+ else COLORS["danger"]
155
158
  )
156
159
  table.add_row(
157
160
  "☔",
158
- "[bold]PRECIPITATION[/bold]",
161
+ "PRECIPITATION",
159
162
  f"{weather.precip_prob}% (6h)",
160
- f"[{COLORS['accent']}]{precip_indicator}[/{COLORS['accent']}]",
163
+ f"[{precip_color}]{precip_category}[/{precip_color}]",
161
164
  )
162
165
 
163
- if headline:
164
- table.add_row(
165
- "",
166
- "[dim]FORECAST[/dim]",
167
- f"[{COLORS['warning']} italic]{headline.upper()}[/{COLORS['warning']} italic]",
168
- "",
169
- )
170
-
171
- # Sunrise/Sunset with day progress
166
+ # Sunrise/Sunset
172
167
  table.add_row(
173
168
  "🌅",
174
- "[bold]SUNRISE[/bold]",
169
+ "SUNRISE",
175
170
  f"[{COLORS['warning']}]{weather.sunrise}[/{COLORS['warning']}]",
176
- "[dim]DAWN[/dim]",
171
+ "",
177
172
  )
178
173
 
179
174
  table.add_row(
180
175
  "🌇",
181
- "[bold]SUNSET[/bold]",
176
+ "SUNSET",
182
177
  f"[{COLORS['danger']}]{weather.sunset}[/{COLORS['danger']}]",
183
- "[dim]DUSK[/dim]",
178
+ "",
184
179
  )
185
180
 
186
- # UV Index with protection level
181
+ # UV Index
187
182
  if weather.uv_index is not None:
188
- uv_label, uv_color = get_uv_guidance(weather.uv_index)
189
- uv_level = min(int(weather.uv_index / 2), 5)
190
- uv_indicator = "☀️" * uv_level
183
+ uv_category, uv_color = get_uv_category(weather.uv_index)
191
184
  table.add_row(
192
185
  "☀️",
193
- "[bold]UV INDEX[/bold]",
186
+ "UV INDEX",
194
187
  f"[{uv_color}]{weather.uv_index:.1f}[/{uv_color}]",
195
- f"[{uv_color}]{uv_label.upper()} {uv_indicator}[/{uv_color}]",
188
+ f"[{uv_color}]{uv_category}[/{uv_color}]",
196
189
  )
197
190
 
198
- # Wind Gust with alert styling
191
+ # Wind Gust
199
192
  if weather.wind_gust is not None:
200
193
  gust_alert = get_wind_gust_alert(weather.wind_speed, weather.wind_gust)
194
+ gust_category, gust_color = get_wind_category_label(
195
+ weather.wind_gust, weather.units.wind
196
+ )
201
197
  if gust_alert:
202
198
  table.add_row(
203
199
  "⚡",
204
200
  "[bold yellow]GUST ALERT[/bold yellow]",
205
201
  f"[bold yellow]{weather.wind_gust} {weather.units.wind}[/bold yellow]",
206
- "[bold yellow]⚠️ SEVERE[/bold yellow]",
202
+ f"[bold yellow]{gust_category} ⚠️[/bold yellow]",
207
203
  )
208
204
  else:
209
- gust_ratio = (
210
- weather.wind_gust / weather.wind_speed if weather.wind_speed > 0 else 1
211
- )
212
- gust_indicator = "!" * min(int(gust_ratio * 2), 3)
213
205
  table.add_row(
214
206
  "💨",
215
- "[bold]WIND GUST[/bold]",
207
+ "WIND GUST",
216
208
  f"{weather.wind_gust} {weather.units.wind}",
217
- f"[{COLORS['steel']}]{gust_indicator}[/{COLORS['steel']}]",
209
+ f"[{gust_color}]{gust_category}[/{gust_color}]",
218
210
  )
219
211
 
220
212
  return table
221
213
 
222
214
 
223
- def create_gauge(value: float, unit: str, max_value: float) -> str:
224
- """Create a text-based gauge visualization."""
225
- if unit == "°F":
226
- ranges = [
227
- (32, "bold cyan"),
228
- (60, "bold blue"),
229
- (80, "bold green"),
230
- (95, "bold yellow"),
231
- (max_value, "bold red"),
232
- ]
233
- else:
234
- ranges = [
235
- (0, "bold cyan"),
236
- (15, "bold blue"),
237
- (26, "bold green"),
238
- (35, "bold yellow"),
239
- (max_value, "bold red"),
240
- ]
241
-
242
- # Find appropriate color
243
- gauge_color = "bold white"
244
- for threshold, color in ranges:
245
- if value <= threshold:
246
- gauge_color = color
247
- break
248
-
249
- # Create gauge visualization
250
- gauge_width = 10
251
- filled = min(int((value / max_value) * gauge_width), gauge_width)
252
- gauge = "[" + gauge_color + "]" + "█" * filled + "[/" + gauge_color + "]"
253
- gauge += "[dim]" + "░" * (gauge_width - filled) + "[/dim]"
254
-
255
- return gauge
256
-
257
-
258
- def get_wind_category(speed: float, unit: str) -> str:
259
- """Categorize wind speed."""
260
- if unit == "mph":
261
- if speed < 1:
262
- return "CALM"
263
- elif speed < 7:
264
- return "LIGHT"
265
- elif speed < 12:
266
- return "GENTLE"
267
- elif speed < 18:
268
- return "MODERATE"
269
- elif speed < 24:
270
- return "FRESH"
271
- elif speed < 31:
272
- return "STRONG"
273
- elif speed < 38:
274
- return "GALE"
275
- elif speed < 46:
276
- return "SEVERE"
277
- else:
278
- return "STORM"
279
- else: # km/h
280
- if speed < 2:
281
- return "CALM"
282
- elif speed < 12:
283
- return "LIGHT"
284
- elif speed < 20:
285
- return "GENTLE"
286
- elif speed < 29:
287
- return "MODERATE"
288
- elif speed < 39:
289
- return "FRESH"
290
- elif speed < 50:
291
- return "STRONG"
292
- elif speed < 62:
293
- return "GALE"
294
- elif speed < 75:
295
- return "SEVERE"
296
- else:
297
- return "STORM"
298
-
299
-
300
215
  def display_weather(weather: WeatherData):
301
216
  """Renders the current conditions weather card for a single city."""
302
217
  if not weather:
@@ -339,19 +254,28 @@ def display_preparation_guidance(weather: WeatherData):
339
254
 
340
255
  if temporal_context:
341
256
  console.print(
342
- f"[{COLORS['secondary']}]┌─ TREND ANALYSIS ─┐[/{COLORS['secondary']}]"
257
+ f"[{COLORS['muted']}]────────────────────────────────────────[/{COLORS['muted']}]"
258
+ )
259
+ console.print(
260
+ f"[{COLORS['primary']} bold]TREND ANALYSIS[/{COLORS['primary']} bold]"
343
261
  )
344
262
  console.print(
345
263
  f"[{COLORS['muted']}]{temporal_context.upper()}[/{COLORS['muted']}]"
346
264
  )
347
265
  console.print(
348
- f"[{COLORS['secondary']}]└──────────────────┘[/{COLORS['secondary']}]"
266
+ f"[{COLORS['muted']}]────────────────────────────────────────[/{COLORS['muted']}]"
349
267
  )
350
268
  print()
351
269
 
352
- # Create industrial guidance panel
270
+ # Create borderless guidance panel
353
271
  console.print(
354
- f"[{COLORS['primary']} bold]┌─ PREPARATION PROTOCOL ─┐[/{COLORS['primary']} bold]"
272
+ f"[{COLORS['muted']}]────────────────────────────────────────[/{COLORS['muted']}]"
273
+ )
274
+ console.print(
275
+ f"[{COLORS['primary']} bold]PREPARATION PROTOCOL[/{COLORS['primary']} bold]"
276
+ )
277
+ console.print(
278
+ f"[{COLORS['muted']}]────────────────────────────────────────[/{COLORS['muted']}]"
355
279
  )
356
280
 
357
281
  # Categorize suggestions by priority
@@ -376,33 +300,31 @@ def display_preparation_guidance(weather: WeatherData):
376
300
  # Display high priority items with danger styling
377
301
  if high_priority:
378
302
  console.print(
379
- f"[{COLORS['danger']} bold] ⚠️ HIGH PRIORITY[/{COLORS['danger']} bold]"
303
+ f"[{COLORS['danger']} bold]⚠️ HIGH PRIORITY[/{COLORS['danger']} bold]"
380
304
  )
381
305
  for suggestion in high_priority:
382
- console.print(
383
- f" [{COLORS['danger']}]▶[/{COLORS['danger']}] {suggestion}"
384
- )
306
+ console.print(f" [{COLORS['danger']}]▶[/{COLORS['danger']}] {suggestion}")
385
307
  print()
386
308
 
387
309
  # Display medium priority items with warning styling
388
310
  if medium_priority:
389
311
  console.print(
390
- f"[{COLORS['warning']} bold] ▲ RECOMMENDED[/{COLORS['warning']} bold]"
312
+ f"[{COLORS['warning']} bold]▲ RECOMMENDED[/{COLORS['warning']} bold]"
391
313
  )
392
314
  for suggestion in medium_priority:
393
315
  console.print(
394
- f" [{COLORS['warning']}]▶[/{COLORS['warning']}] {suggestion}"
316
+ f" [{COLORS['warning']}]▶[/{COLORS['warning']}] {suggestion}"
395
317
  )
396
318
  print()
397
319
 
398
320
  # Display low priority items with muted styling
399
321
  if low_priority:
400
- console.print(f"[{COLORS['muted']} bold] ○ ADVISORY[/{COLORS['muted']} bold]")
322
+ console.print(f"[{COLORS['muted']} bold]○ ADVISORY[/{COLORS['muted']} bold]")
401
323
  for suggestion in low_priority:
402
- console.print(f" [{COLORS['muted']}]▶[/{COLORS['muted']}] {suggestion}")
324
+ console.print(f" [{COLORS['muted']}]▶[/{COLORS['muted']}] {suggestion}")
403
325
 
404
326
  console.print(
405
- f"[{COLORS['primary']} bold]└─────────────────────────┘[/{COLORS['primary']} bold]"
327
+ f"[{COLORS['muted']}]────────────────────────────────────────[/{COLORS['muted']}]"
406
328
  )
407
329
 
408
330
 
@@ -425,39 +347,34 @@ def display_multi_weather(weather_list: list[WeatherData]):
425
347
 
426
348
 
427
349
  def display_forecast(weather: WeatherData):
428
- """Renders a 7-day forecast table with industrial aesthetic."""
350
+ """Renders a 7-day forecast table with borderless industrial aesthetic."""
429
351
  if not weather.forecast:
430
352
  console.print(
431
353
  f"[{COLORS['warning']}]No forecast data available.[/{COLORS['warning']}]"
432
354
  )
433
355
  return
434
356
 
435
- # Create industrial forecast table
357
+ # Create borderless forecast table
436
358
  table = Table(
437
- title=f"[{COLORS['primary']} bold]┌─ FORECAST PANEL ─┐[/{COLORS['primary']} bold]\n"
438
- f"[{COLORS['steel']}]{weather.city.upper()}[/{COLORS['steel']}]",
359
+ title=f"[{COLORS['primary']} bold]FORECAST PANEL · {weather.city.upper()}[/{COLORS['primary']} bold]",
439
360
  show_header=True,
361
+ box=None, # No borders
440
362
  header_style=f"{COLORS['muted']} bold",
441
- box=box.HEAVY,
442
- border_style=COLORS["secondary"],
443
363
  padding=(0, 1),
444
364
  width=70,
445
365
  )
446
366
 
447
- # Industrial column headers
448
- table.add_column("DAY", justify="left", width=12, style=f"{COLORS['accent']} bold")
367
+ # Borderless column headers
368
+ table.add_column("DAY", justify="left", width=10, style=f"{COLORS['accent']} bold")
449
369
  table.add_column("", justify="center", width=2, style=COLORS["accent"])
450
- table.add_column("CONDITIONS", justify="left", width=20, style=COLORS["muted"])
370
+ table.add_column("CONDITIONS", justify="left", width=18, style=COLORS["muted"])
451
371
  table.add_column(
452
- "HIGH", justify="right", width=10, style=f"{COLORS['primary']} bold"
453
- )
454
- table.add_column(
455
- "LOW", justify="right", width=10, style=f"{COLORS['primary']} bold"
372
+ "HIGH", justify="right", width=8, style=f"{COLORS['primary']} bold"
456
373
  )
374
+ table.add_column("LOW", justify="right", width=8, style=f"{COLORS['primary']} bold")
457
375
  table.add_column(
458
376
  "RAIN%", justify="right", width=8, style=f"{COLORS['accent']} bold"
459
377
  )
460
- table.add_column("STATUS", justify="center", width=8, style=COLORS["secondary"])
461
378
 
462
379
  for i, day in enumerate(weather.forecast):
463
380
  day_label = "TODAY" if i == 0 else day.dt.strftime("%a").upper()
@@ -467,7 +384,7 @@ def display_forecast(weather: WeatherData):
467
384
  high_color = get_temp_color(day.high, weather.units.temp)
468
385
  low_color = get_temp_color(day.low, weather.units.temp)
469
386
 
470
- # Precipitation severity indicator
387
+ # Precipitation color based on probability
471
388
  if day.precip_prob >= 70:
472
389
  rain_color = COLORS["danger"]
473
390
  elif day.precip_prob >= 40:
@@ -477,17 +394,6 @@ def display_forecast(weather: WeatherData):
477
394
  else:
478
395
  rain_color = COLORS["muted"]
479
396
 
480
- # Day severity based on weather conditions
481
- if day.weather_code in [95, 96, 99, 65, 75, 86]:
482
- day_severity = "▲"
483
- severity_color = COLORS["warning"]
484
- elif day.precip_prob > 60:
485
- day_severity = "●"
486
- severity_color = COLORS["accent"]
487
- else:
488
- day_severity = "○"
489
- severity_color = COLORS["success"]
490
-
491
397
  table.add_row(
492
398
  f"[{COLORS['primary']} bold]{day_label}[/{COLORS['primary']} bold]"
493
399
  if i == 0
@@ -497,7 +403,6 @@ def display_forecast(weather: WeatherData):
497
403
  f"[{high_color}]{day.high}{weather.units.temp}[/{high_color}]",
498
404
  f"[{low_color}]{day.low}{weather.units.temp}[/{low_color}]",
499
405
  f"[{rain_color}]{day.precip_prob}%[/{rain_color}]",
500
- f"[{severity_color}]{day_severity}[/{severity_color}]",
501
406
  )
502
407
 
503
408
  console.print(table)
@@ -505,49 +410,40 @@ def display_forecast(weather: WeatherData):
505
410
 
506
411
 
507
412
  def display_hourly(weather: WeatherData):
508
- """Renders an hourly forecast table with industrial aesthetic."""
413
+ """Renders an hourly forecast table with borderless industrial aesthetic."""
509
414
  if not weather.hourly:
510
415
  console.print(
511
416
  f"[{COLORS['warning']}]No hourly data available.[/{COLORS['warning']}]"
512
417
  )
513
418
  return
514
419
 
515
- # Create industrial hourly table
420
+ # Create borderless hourly table
516
421
  table = Table(
517
- title=f"[{COLORS['primary']} bold]┌─ HOURLY TRACKER ─┐[/{COLORS['primary']} bold]\n"
518
- f"[{COLORS['steel']}]{weather.city.upper()}[/{COLORS['steel']}]",
422
+ title=f"[{COLORS['primary']} bold]HOURLY FORECAST · {weather.city.upper()}[/{COLORS['primary']} bold]",
519
423
  show_header=True,
424
+ box=None, # No borders
520
425
  header_style=f"{COLORS['muted']} bold",
521
- box=box.HEAVY,
522
- border_style=COLORS["secondary"],
523
426
  padding=(0, 1),
524
- width=65,
427
+ width=60,
525
428
  )
526
429
 
527
- # Industrial column headers
528
- table.add_column("TIME", justify="left", width=8, style=f"{COLORS['accent']} bold")
430
+ # Simplified column headers
431
+ table.add_column("TIME", justify="left", width=6, style=f"{COLORS['accent']} bold")
529
432
  table.add_column("", justify="center", width=2, style=COLORS["accent"])
530
433
  table.add_column(
531
- "TEMP", justify="right", width=10, style=f"{COLORS['primary']} bold"
434
+ "TEMP", justify="right", width=8, style=f"{COLORS['primary']} bold"
532
435
  )
533
- table.add_column("GAUGE", justify="left", width=12, style=COLORS["muted"])
534
436
  table.add_column(
535
- "RAIN%", justify="right", width=8, style=f"{COLORS['accent']} bold"
437
+ "RAIN%", justify="right", width=6, style=f"{COLORS['accent']} bold"
536
438
  )
537
- table.add_column("CONDITIONS", justify="left", width=20, style=COLORS["concrete"])
538
- table.add_column("", justify="center", width=2, style=COLORS["secondary"])
439
+ table.add_column("CONDITIONS", justify="left", width=18, style=COLORS["concrete"])
539
440
 
540
441
  for hour in weather.hourly[:12]:
541
442
  time_label = hour.dt.strftime("%-I%p").upper()
542
443
  icon, desc = get_weather_icon(hour.weather_code)
543
444
  temp_color = get_temp_color(hour.temp, weather.units.temp)
544
445
 
545
- # Temperature gauge
546
- temp_gauge = create_gauge(
547
- hour.temp, weather.units.temp, 100 if weather.units.temp == "°F" else 40
548
- )
549
-
550
- # Precipitation indicator
446
+ # Precipitation color
551
447
  if hour.precip_prob >= 70:
552
448
  rain_color = COLORS["danger"]
553
449
  elif hour.precip_prob >= 40:
@@ -557,25 +453,12 @@ def display_hourly(weather: WeatherData):
557
453
  else:
558
454
  rain_color = COLORS["muted"]
559
455
 
560
- # Hour severity indicator
561
- if hour.weather_code in [95, 96, 99]:
562
- severity_icon = "⚡"
563
- severity_color = COLORS["warning"]
564
- elif hour.precip_prob > 60:
565
- severity_icon = "☔"
566
- severity_color = COLORS["accent"]
567
- else:
568
- severity_icon = "○"
569
- severity_color = COLORS["success"]
570
-
571
456
  table.add_row(
572
457
  f"[{COLORS['muted']}]{time_label}[/{COLORS['muted']}]",
573
458
  f"[{COLORS['accent']}]{icon}[/{COLORS['accent']}]",
574
459
  f"[{temp_color}]{hour.temp}{weather.units.temp}[/{temp_color}]",
575
- temp_gauge,
576
460
  f"[{rain_color}]{hour.precip_prob}%[/{rain_color}]",
577
461
  f"[{COLORS['concrete']}]{desc.upper()}[/{COLORS['concrete']}]",
578
- f"[{severity_color}]{severity_icon}[/{severity_color}]",
579
462
  )
580
463
 
581
464
  console.print(table)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: isobar-cli
3
- Version: 1.3.2
3
+ Version: 1.3.3
4
4
  Summary: A terminal weather tool with industrial aesthetic, focusing on Real Feel and Windchill.
5
5
  Author: Beau Bremer / KnowOneActual
6
6
  License-Expression: MIT
@@ -380,8 +380,8 @@ def test_main_with_flags(monkeypatch):
380
380
  env={"TERM": "dumb", "NO_COLOR": "1"},
381
381
  )
382
382
  assert result.exit_code == 0
383
- # In industrial aesthetic, hourly display has "HOURLY TRACKER" header
384
- assert "HOURLY TRACKER" in result.output.upper()
383
+ # In borderless industrial aesthetic, hourly display has "HOURLY FORECAST" header
384
+ assert "HOURLY FORECAST" in result.output.upper()
385
385
  assert "CITYH" in result.output.upper()
386
386
 
387
387
  # Forecast
@@ -572,8 +572,8 @@ def test_display_multi_weather(mock_cache_dir):
572
572
 
573
573
 
574
574
  def test_get_precip_headline_extra():
575
- assert ui.get_precip_headline(80, 0.5, 0, "in") == "Moderate rain likely"
576
- assert ui.get_precip_headline(80, 0.1, 0, "in") == "Light rain likely"
575
+ assert logic.get_precip_headline(80, 0.5, 0, "in") == "Moderate rain likely"
576
+ assert logic.get_precip_headline(80, 0.1, 0, "in") == "Light rain likely"
577
577
 
578
578
 
579
579
  def test_build_weather_table_extra():
@@ -1,8 +1,8 @@
1
+ from isobar_cli.logic import get_precip_headline
1
2
  from isobar_cli.models import WeatherData, WeatherUnits
2
3
  from isobar_cli.ui import (
3
4
  build_weather_table,
4
5
  get_aqi_label,
5
- get_precip_headline,
6
6
  get_temp_color,
7
7
  get_weather_icon,
8
8
  )
@@ -1 +0,0 @@
1
- __version__ = "1.3.2"
File without changes
File without changes
File without changes
File without changes