@@ -143,6 +143,63 @@ const (
143143 fieldCount
144144)
145145
146+ // Layout constants
147+ const (
148+ minWidth = 60
149+ maxTableWidth = 160
150+ headerHeight = 4 // Logo + spacing
151+ footerHeight = 4 // Help + status
152+ minTableHeight = 5
153+ formHeaderHeight = 4
154+ formFooterHeight = 6
155+ outputHeaderHeight = 5
156+ outputFooterHeight = 3
157+ )
158+
159+ // calculateTableColumns returns column definitions sized for the given width
160+ func calculateTableColumns (width int ) []table.Column {
161+ // Account for table borders and padding
162+ availableWidth := width - 4
163+ if availableWidth < minWidth {
164+ availableWidth = minWidth
165+ }
166+ if availableWidth > maxTableWidth {
167+ availableWidth = maxTableWidth
168+ }
169+
170+ // Column proportions (percentages): Name 25%, Schedule 20%, Status 12%, Next 20%, Last 20%
171+ // Status is fixed width since it's short text
172+ statusWidth := 10
173+ remaining := availableWidth - statusWidth - 8 // 8 for column separators
174+
175+ nameWidth := remaining * 25 / 85
176+ scheduleWidth := remaining * 20 / 85
177+ nextWidth := remaining * 20 / 85
178+ lastWidth := remaining * 20 / 85
179+
180+ // Ensure minimum widths
181+ if nameWidth < 12 {
182+ nameWidth = 12
183+ }
184+ if scheduleWidth < 15 {
185+ scheduleWidth = 15
186+ }
187+ if nextWidth < 14 {
188+ nextWidth = 14
189+ }
190+ if lastWidth < 14 {
191+ lastWidth = 14
192+ }
193+
194+ return []table.Column {
195+ {Title : "Name" , Width : nameWidth },
196+ {Title : "Schedule" , Width : scheduleWidth },
197+ {Title : "Status" , Width : statusWidth },
198+ {Title : "Next Run" , Width : nextWidth },
199+ {Title : "Last Run" , Width : lastWidth },
200+ }
201+ }
202+
146203// NewModel creates a new TUI model
147204func NewModel (database * db.DB , sched * scheduler.Scheduler ) Model {
148205 // Spinner
@@ -155,19 +212,13 @@ func NewModel(database *db.DB, sched *scheduler.Scheduler) Model {
155212 h .Styles .ShortKey = helpKeyStyle
156213 h .Styles .ShortDesc = helpDescStyle
157214
158- // Table
159- columns := []table.Column {
160- {Title : "Name" , Width : 20 },
161- {Title : "Schedule" , Width : 20 },
162- {Title : "Status" , Width : 12 },
163- {Title : "Next Run" , Width : 20 },
164- {Title : "Last Run" , Width : 20 },
165- }
215+ // Table - start with reasonable default, will resize on WindowSizeMsg
216+ columns := calculateTableColumns (100 )
166217
167218 t := table .New (
168219 table .WithColumns (columns ),
169220 table .WithFocused (true ),
170- table .WithHeight (7 ),
221+ table .WithHeight (10 ),
171222 )
172223
173224 ts := table .DefaultStyles ()
@@ -224,35 +275,83 @@ func NewModel(database *db.DB, sched *scheduler.Scheduler) Model {
224275func (m * Model ) initFormInputs () {
225276 m .formInputs = make ([]textinput.Model , fieldCount )
226277
278+ // Calculate responsive width (will be updated on WindowSizeMsg)
279+ inputWidth := m .getFormInputWidth ()
280+
227281 m .formInputs [fieldName ] = textinput .New ()
228282 m .formInputs [fieldName ].Placeholder = "Daily code review"
229283 m .formInputs [fieldName ].CharLimit = 100
230- m .formInputs [fieldName ].Width = 50
284+ m .formInputs [fieldName ].Width = inputWidth
231285
232286 // Prompt uses textarea for multi-line input
233287 m .promptInput = textarea .New ()
234288 m .promptInput .Placeholder = "Review recent changes and summarize..."
235289 m .promptInput .CharLimit = 2000
236- m .promptInput .SetWidth (52 )
237- m .promptInput .SetHeight (6 )
290+ m .promptInput .SetWidth (inputWidth + 2 )
291+ m .promptInput .SetHeight (m . getTextareaHeight () )
238292 m .promptInput .ShowLineNumbers = false
239293
240294 m .formInputs [fieldCron ] = textinput .New ()
241295 m .formInputs [fieldCron ].Placeholder = "0 * * * * * (every minute)"
242296 m .formInputs [fieldCron ].CharLimit = 50
243- m .formInputs [fieldCron ].Width = 50
297+ m .formInputs [fieldCron ].Width = inputWidth
244298
245299 m .formInputs [fieldWorkingDir ] = textinput .New ()
246300 m .formInputs [fieldWorkingDir ].Placeholder = "/path/to/project"
247301 m .formInputs [fieldWorkingDir ].CharLimit = 500
248- m .formInputs [fieldWorkingDir ].Width = 50
302+ m .formInputs [fieldWorkingDir ].Width = inputWidth
249303 wd , _ := os .Getwd ()
250304 m .formInputs [fieldWorkingDir ].SetValue (wd )
251305
252306 m .formInputs [fieldDiscordWebhook ] = textinput .New ()
253307 m .formInputs [fieldDiscordWebhook ].Placeholder = "https://discord.com/api/webhooks/..."
254308 m .formInputs [fieldDiscordWebhook ].CharLimit = 500
255- m .formInputs [fieldDiscordWebhook ].Width = 50
309+ m .formInputs [fieldDiscordWebhook ].Width = inputWidth
310+ }
311+
312+ // getFormInputWidth calculates responsive input width
313+ func (m * Model ) getFormInputWidth () int {
314+ if m .width == 0 {
315+ return 50 // default before first WindowSizeMsg
316+ }
317+ // Use ~80% of available width, with min/max bounds
318+ width := (m .width - 8 ) * 80 / 100
319+ if width < 40 {
320+ width = 40
321+ }
322+ if width > 100 {
323+ width = 100
324+ }
325+ return width
326+ }
327+
328+ // getTextareaHeight calculates responsive textarea height
329+ func (m * Model ) getTextareaHeight () int {
330+ if m .height == 0 {
331+ return 6 // default before first WindowSizeMsg
332+ }
333+ // Calculate available height for form
334+ // Each field takes ~3 lines (label + input + spacing)
335+ otherFieldsHeight := 4 * 3 // 4 other fields
336+ availableForTextarea := m .height - formHeaderHeight - formFooterHeight - otherFieldsHeight - 4
337+ if availableForTextarea < 4 {
338+ availableForTextarea = 4
339+ }
340+ if availableForTextarea > 12 {
341+ availableForTextarea = 12
342+ }
343+ return availableForTextarea
344+ }
345+
346+ // updateFormWidths updates all form input widths for new terminal size
347+ func (m * Model ) updateFormWidths (width int ) {
348+ inputWidth := m .getFormInputWidth ()
349+
350+ for i := range m .formInputs {
351+ m .formInputs [i ].Width = inputWidth
352+ }
353+ m .promptInput .SetWidth (inputWidth + 2 )
354+ m .promptInput .SetHeight (m .getTextareaHeight ())
256355}
257356
258357func (m * Model ) resetForm () {
@@ -284,6 +383,15 @@ func (m *Model) updateTable() {
284383 return
285384 }
286385
386+ // Get current column widths for truncation
387+ columns := m .table .Columns ()
388+ nameWidth := 18
389+ scheduleWidth := 18
390+ if len (columns ) >= 2 {
391+ nameWidth = columns [0 ].Width - 2 // leave room for ellipsis
392+ scheduleWidth = columns [1 ].Width - 2
393+ }
394+
287395 rows := make ([]table.Row , len (m .tasks ))
288396 for i , task := range m .tasks {
289397 status := "disabled"
@@ -304,22 +412,13 @@ func (m *Model) updateTable() {
304412 }
305413
306414 rows [i ] = table.Row {
307- truncate (task .Name , 18 ),
308- truncate (task .CronExpr , 18 ),
415+ truncate (task .Name , nameWidth ),
416+ truncate (task .CronExpr , scheduleWidth ),
309417 status ,
310418 nextRun ,
311419 lastRun ,
312420 }
313421 }
314- // Set height first, then rows
315- h := len (m .tasks )
316- if h < 5 {
317- h = 5
318- }
319- if h > 15 {
320- h = 15
321- }
322- m .table .SetHeight (h )
323422 m .table .SetRows (rows )
324423}
325424
@@ -438,19 +537,40 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
438537 case tea.WindowSizeMsg :
439538 m .width = msg .Width
440539 m .height = msg .Height
441- m .table .SetWidth (msg .Width - 4 )
442- // Set reasonable table height
443- h := msg .Height - 15
444- if h < 5 {
445- h = 5
540+
541+ // Update table columns and dimensions
542+ m .table .SetColumns (calculateTableColumns (msg .Width ))
543+ tableWidth := msg .Width - 4
544+ if tableWidth > maxTableWidth {
545+ tableWidth = maxTableWidth
546+ }
547+ m .table .SetWidth (tableWidth )
548+
549+ // Calculate table height based on available space
550+ // Account for header, running indicator (2 lines if shown), status, and help
551+ runningIndicatorHeight := 0
552+ if len (m .runningTasks ) > 0 {
553+ runningIndicatorHeight = 2
446554 }
447- if h > 20 {
448- h = 20
555+ availableHeight := msg .Height - headerHeight - footerHeight - runningIndicatorHeight - 2 // 2 for app padding
556+ if availableHeight < minTableHeight {
557+ availableHeight = minTableHeight
558+ }
559+ m .table .SetHeight (availableHeight )
560+
561+ // Update viewport for output view
562+ viewportHeight := msg .Height - outputHeaderHeight - outputFooterHeight - 2
563+ if viewportHeight < 5 {
564+ viewportHeight = 5
449565 }
450- m .table .SetHeight (h )
451566 m .viewport .Width = msg .Width - 6
452- m .viewport .Height = msg .Height - 12
567+ m .viewport .Height = viewportHeight
568+
453569 m .help .Width = msg .Width
570+
571+ // Update form input widths
572+ m .updateFormWidths (msg .Width )
573+
454574 // Update markdown renderer for new width
455575 if renderer , err := glamour .NewTermRenderer (
456576 glamour .WithAutoStyle (),
0 commit comments