@@ -52,19 +52,20 @@ var urlRegex *regexp.Regexp
5252
5353// keyMap defines all keybindings for the help system
5454type keyMap struct {
55- Send key.Binding
56- ScrollUp key.Binding
57- ScrollDown key.Binding
58- PageUp key.Binding
59- PageDown key.Binding
60- Copy key.Binding
61- Paste key.Binding
62- Cut key.Binding
63- SelectAll key.Binding
64- Help key.Binding
65- Quit key.Binding
66- TimeFormat key.Binding
67- Clear key.Binding
55+ Send key.Binding
56+ ScrollUp key.Binding
57+ ScrollDown key.Binding
58+ PageUp key.Binding
59+ PageDown key.Binding
60+ Copy key.Binding
61+ Paste key.Binding
62+ Cut key.Binding
63+ SelectAll key.Binding
64+ Help key.Binding
65+ Quit key.Binding
66+ TimeFormat key.Binding
67+ Clear key.Binding
68+ QuitCommand key.Binding
6869 // Commands with both text commands and hotkey alternatives
6970 SendFile key.Binding
7071 SaveFile key.Binding
@@ -176,7 +177,7 @@ func newKeyMap() keyMap {
176177 ),
177178 Quit : key .NewBinding (
178179 key .WithKeys ("esc" ),
179- key .WithHelp ("esc" , "quit " ),
180+ key .WithHelp ("esc" , "close menus " ),
180181 ),
181182 TimeFormat : key .NewBinding (
182183 key .WithKeys (":time" ),
@@ -186,6 +187,10 @@ func newKeyMap() keyMap {
186187 key .WithKeys (":clear" ),
187188 key .WithHelp (":clear" , "clear chat history" ),
188189 ),
190+ QuitCommand : key .NewBinding (
191+ key .WithKeys (":q" ),
192+ key .WithHelp (":q" , "quit client" ),
193+ ),
189194 SendFile : key .NewBinding (
190195 key .WithKeys (":sendfile" ),
191196 key .WithHelp (":sendfile <path>" , "send a file" ),
@@ -572,9 +577,62 @@ func debugWebSocketWrite(ws *websocket.Conn, msg interface{}) error {
572577 return ws .WriteJSON (msg )
573578}
574579
580+ // getCurrentProfileName finds the profile name that matches the current config
581+ func getCurrentProfileName (cfg * config.Config ) string {
582+ loader , err := config .NewInteractiveConfigLoader ()
583+ if err != nil {
584+ return "" // Can't determine profile name
585+ }
586+
587+ profiles , err := loader .LoadProfiles ()
588+ if err != nil {
589+ return "" // Can't load profiles
590+ }
591+
592+ // Find matching profile
593+ for _ , profile := range profiles .Profiles {
594+ if profile .Username == cfg .Username &&
595+ profile .ServerURL == cfg .ServerURL &&
596+ profile .IsAdmin == cfg .IsAdmin &&
597+ profile .UseE2E == cfg .UseE2E {
598+ return profile .Name
599+ }
600+ }
601+
602+ return "" // No matching profile found
603+ }
604+
605+ // updateProfileTheme updates the theme in the corresponding profile in profiles.json
606+ func (m * model ) updateProfileTheme (newTheme string ) error {
607+ if m .profileName == "" {
608+ return nil // No profile to update
609+ }
610+
611+ loader , err := config .NewInteractiveConfigLoader ()
612+ if err != nil {
613+ return err
614+ }
615+
616+ profiles , err := loader .LoadProfiles ()
617+ if err != nil {
618+ return err
619+ }
620+
621+ // Find and update the matching profile
622+ for i , profile := range profiles .Profiles {
623+ if profile .Name == m .profileName {
624+ profiles .Profiles [i ].Theme = newTheme
625+ return loader .SaveProfiles (profiles )
626+ }
627+ }
628+
629+ return nil // Profile not found, nothing to update
630+ }
631+
575632type model struct {
576633 cfg config.Config
577634 configFilePath string // Store the config file path for saving
635+ profileName string // Store the current profile name for updating profiles.json
578636 textarea textarea.Model
579637 viewport viewport.Model
580638 messages []shared.Message
@@ -1439,8 +1497,8 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
14391497 m .selectedUser = ""
14401498 return m , nil
14411499 }
1442- m . closeWebSocket ()
1443- return m , tea . Quit
1500+ // ESC no longer quits - use :q command instead
1501+ return m , nil
14441502 case key .Matches (v , m .keys .DatabaseMenu ):
14451503 // Only show database menu if admin and no other menus are open
14461504 if * isAdmin && ! m .showHelp {
@@ -1515,13 +1573,21 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
15151573 }
15161574 }
15171575 nextIndex := (currentIndex + 1 ) % len (themes )
1518- m .cfg .Theme = themes [nextIndex ]
1576+ newTheme := themes [nextIndex ]
1577+ m .cfg .Theme = newTheme
15191578 m .styles = getThemeStyles (m .cfg .Theme )
15201579 _ = config .SaveConfig (m .configFilePath , m .cfg )
15211580
1581+ // Update profile with new theme
1582+ _ = m .updateProfileTheme (newTheme )
1583+
15221584 // Show theme info in banner
15231585 themeInfo := GetThemeInfo (m .cfg .Theme )
15241586 m .banner = fmt .Sprintf ("Theme: %s" , themeInfo )
1587+
1588+ // Redraw viewport and user list with new theme
1589+ m .viewport .SetContent (renderMessages (m .messages , m .styles , m .cfg .Username , m .users , m .viewport .Width , m .twentyFourHour ))
1590+ m .userListViewport .SetContent (renderUserList (m .users , m .cfg .Username , m .styles , m .width , * isAdmin , m .selectedUserIndex ))
15251591 return m , nil
15261592 case key .Matches (v , m .keys .TimeFormatHotkey ):
15271593 // Toggle time format
@@ -1926,23 +1992,43 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
19261992 if len (parts ) == 2 && strings .TrimSpace (parts [1 ]) != "" {
19271993 themeName := strings .TrimSpace (parts [1 ])
19281994
1929- // Check if theme exists
1995+ // Check if theme exists using case-insensitive lookup
19301996 allThemes := ListAllThemes ()
19311997 themeExists := false
1998+ actualThemeName := themeName
1999+
2000+ // First try exact match
19322001 for _ , t := range allThemes {
19332002 if t == themeName {
19342003 themeExists = true
2004+ actualThemeName = t
19352005 break
19362006 }
19372007 }
19382008
2009+ // If not found, try case-insensitive match for custom themes
2010+ if ! themeExists {
2011+ if key , _ , found := GetCustomThemeByName (themeName ); found {
2012+ themeExists = true
2013+ actualThemeName = key
2014+ }
2015+ }
2016+
19392017 if ! themeExists {
19402018 m .banner = fmt .Sprintf ("Theme '%s' not found. Use :themes to list available themes." , themeName )
19412019 } else {
1942- m .cfg .Theme = themeName
2020+ m .cfg .Theme = actualThemeName
19432021 m .styles = getThemeStyles (m .cfg .Theme )
19442022 _ = config .SaveConfig (m .configFilePath , m .cfg )
1945- m .banner = fmt .Sprintf ("Theme changed to: %s" , GetThemeInfo (themeName ))
2023+
2024+ // Update profile with new theme
2025+ _ = m .updateProfileTheme (actualThemeName )
2026+
2027+ m .banner = fmt .Sprintf ("Theme changed to: %s" , GetThemeInfo (actualThemeName ))
2028+
2029+ // Redraw viewport and user list with new theme
2030+ m .viewport .SetContent (renderMessages (m .messages , m .styles , m .cfg .Username , m .users , m .viewport .Width , m .twentyFourHour ))
2031+ m .userListViewport .SetContent (renderUserList (m .users , m .cfg .Username , m .styles , m .width , * isAdmin , m .selectedUserIndex ))
19462032 }
19472033 } else {
19482034 m .banner = "Please provide a theme name. Use :themes to list available themes."
@@ -1957,6 +2043,11 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
19572043 m .textarea .SetValue ("" )
19582044 return m , nil
19592045 }
2046+ if text == ":q" {
2047+ m .closeWebSocket ()
2048+ m .textarea .SetValue ("" )
2049+ return m , tea .Quit
2050+ }
19602051 // Individual E2E encryption commands removed - only global E2E encryption supported
19612052 if text == ":time" {
19622053 m .twentyFourHour = ! m .twentyFourHour
@@ -2183,7 +2274,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
21832274 if m .conn != nil {
21842275 // Check if this is a server-side command (admin/plugin) that should bypass encryption
21852276 // Client-side commands are handled above and never reach this point
2186- clientOnlyCommands := []string {":theme" , ":time" , ":clear" , ":bell" , ":bell-mention" , ":code" , ":sendfile" , ":savefile" }
2277+ clientOnlyCommands := []string {":theme" , ":time" , ":clear" , ":bell" , ":bell-mention" , ":code" , ":sendfile" , ":savefile" , ":q" }
21872278 isClientCommand := false
21882279 for _ , cmd := range clientOnlyCommands {
21892280 // Check if text is exactly the command or starts with "command "
@@ -3346,6 +3437,7 @@ func initializeClient(cfg *config.Config, adminKeyParam, keystorePassphraseParam
33463437 m := & model {
33473438 cfg : * cfg ,
33483439 configFilePath : configFilePath ,
3440+ profileName : getCurrentProfileName (cfg ),
33493441 textarea : ta ,
33503442 viewport : vp ,
33513443 styles : getThemeStyles (cfg .Theme ),
0 commit comments