Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Changes In Branch make-themes-reloadable Excluding Merge-Ins

This is equivalent to a diff from b21a014f8d to 619d1c3371

2024-08-22
08:59
Merge and integrate the make-themes-reloadable branch check-in: 14d6a14000 user: alexa tags: trunk
08:58
Go back to using an Atomic, but this time an `Atomic(Theme?)`. My earlier suspicion seems to be correct. Closed-Leaf check-in: 619d1c3371 user: alexa tags: make-themes-reloadable
08:55
Reorganize some stuff check-in: dd1d312062 user: alexa tags: make-themes-reloadable
08:34
Merge from trunk check-in: ef831f2480 user: alexa tags: make-themes-reloadable
08:29
Allocate fewer strings check-in: b21a014f8d user: alexa tags: trunk
08:26
Allocate a bit less check-in: 0cb9c4bf16 user: alexa tags: trunk

Changes to src/common.cr.
149
150
151
152
153
154
155

156
157
158
159
160
161
162
    Exit
    StopAfterCurrent
    ReloadSongConfigs
    ToggleInterpolation
    ToggleChorus
    SeekForward
    SeekBackward

  end

  enum ProgramState
    Run
    Quit
  end








>







149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
    Exit
    StopAfterCurrent
    ReloadSongConfigs
    ToggleInterpolation
    ToggleChorus
    SeekForward
    SeekBackward
    ReloadTheme
  end

  enum ProgramState
    Run
    Quit
  end

Changes to src/config/theme.cr.
63
64
65
66
67
68
69






70
71
72
73
74
75
76
77

    Config.confField(vuBarChar,            String,     DEF_VU_BAR_CHAR.to_s,     key: "vu-bar-character")
    Config.confField(vuTipChar,            String,     DEF_VU_TIP_CHAR.to_s,     key: "vu-tip-character")
    Config.confField(vuColors,      Array(ThemeColor), DEF_VU_COLORS,            key: "vu-colors")
    Config.confField(vuClipColor,          ThemeColor, DEF_VU_CLIP_COLOR,        key: "vu-clip-color")
    Config.confField(vuClippedChannelTime, UInt8,      DEF_CLIPPED_CHANNEL_TIME, key: "vu-clipped-channel-time")







    def initialize
    end

    def after_initialize
      {% begin %}
        raise ThemeError.new("Unsupported theme: version is too high") if @version > THEME_FORMAT_VERSION

        case @version







>
>
>
>
>
>
|







63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

    Config.confField(vuBarChar,            String,     DEF_VU_BAR_CHAR.to_s,     key: "vu-bar-character")
    Config.confField(vuTipChar,            String,     DEF_VU_TIP_CHAR.to_s,     key: "vu-tip-character")
    Config.confField(vuColors,      Array(ThemeColor), DEF_VU_COLORS,            key: "vu-colors")
    Config.confField(vuClipColor,          ThemeColor, DEF_VU_CLIP_COLOR,        key: "vu-clip-color")
    Config.confField(vuClippedChannelTime, UInt8,      DEF_CLIPPED_CHANNEL_TIME, key: "vu-clipped-channel-time")

    @[YAML::Field(ignore: true)]
    getter name : String = ""

    protected def name=(@name : String)
    end

    def initialize(@name = "")
    end

    def after_initialize
      {% begin %}
        raise ThemeError.new("Unsupported theme: version is too high") if @version > THEME_FORMAT_VERSION

        case @version
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
        else
          raise ThemeError.new("Unsupported theme version")
        end
      {% end %}
    end

    def dup
      ret : Theme = Theme.new
      ret.bgColor = self.bgColor
      ret.headerColor = self.headerColor
      ret.errColor = self.errColor
      ret.curSongColor = self.curSongColor
      ret.prevSongColor = self.prevSongColor
      ret.nextSongColor1 = self.nextSongColor1
      ret.nextSongColor2 = self.nextSongColor2







|







147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
        else
          raise ThemeError.new("Unsupported theme version")
        end
      {% end %}
    end

    def dup
      ret : Theme = Theme.new(@name)
      ret.bgColor = self.bgColor
      ret.headerColor = self.headerColor
      ret.errColor = self.errColor
      ret.curSongColor = self.curSongColor
      ret.prevSongColor = self.prevSongColor
      ret.nextSongColor1 = self.nextSongColor1
      ret.nextSongColor2 = self.nextSongColor2
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199


200
201
202
203
204
205
206
207
208


209
210
211
212
213
214
215
    # Attempts to load a theme file from disk.
    def self.load(resolver, aName : String|Array(String)) : Theme
      name : String = case aName
                      in String then aName
                      in Array(String) then aName.sample
                      end

      return Theme.new if name == DEF_THEME_NAME

      # All theme files use the same format: theme-<name of theme>.yaml
      path = resolver.configDir.join(THEMES_DIR_NAME).join("theme-#{name}.yaml")
      if File.exists?(path)
        File.open(path, "r") do |file|
          RemiLib.log.log("Loading user theme: #{name}")
          return Theme.from_yaml(file)


        end
      end

      # Check system-wide
      path = BENBEN_SYSTEM_DATA_DIR.join(SHORT_NAME, THEMES_DIR_NAME, "theme-#{name}.yaml")
      if File.exists?(path)
        File.open(path, "r") do |file|
          RemiLib.log.log("Loading system-wide theme: #{name}")
          return Theme.from_yaml(file)


        end
      end

      raise ThemeError.new("Cannot find theme: #{name}")
    end

    # Generates a list of all available themes.







|






|
>
>








|
>
>







191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
    # Attempts to load a theme file from disk.
    def self.load(resolver, aName : String|Array(String)) : Theme
      name : String = case aName
                      in String then aName
                      in Array(String) then aName.sample
                      end

      return Theme.new(DEF_THEME_NAME) if name == DEF_THEME_NAME

      # All theme files use the same format: theme-<name of theme>.yaml
      path = resolver.configDir.join(THEMES_DIR_NAME).join("theme-#{name}.yaml")
      if File.exists?(path)
        File.open(path, "r") do |file|
          RemiLib.log.log("Loading user theme: #{name}")
          ret = Theme.from_yaml(file)
          ret.name = name
          return ret
        end
      end

      # Check system-wide
      path = BENBEN_SYSTEM_DATA_DIR.join(SHORT_NAME, THEMES_DIR_NAME, "theme-#{name}.yaml")
      if File.exists?(path)
        File.open(path, "r") do |file|
          RemiLib.log.log("Loading system-wide theme: #{name}")
          ret = Theme.from_yaml(file)
          ret.name = name
          return ret
        end
      end

      raise ThemeError.new("Cannot find theme: #{name}")
    end

    # Generates a list of all available themes.
233
234
235
236
237
238
239












240
241

            ret[match[1]] = true
          end
        end
      end

      ret.keys
    end












  end
end








>
>
>
>
>
>
>
>
>
>
>
>
|
|
>
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
            ret[match[1]] = true
          end
        end
      end

      ret.keys
    end

    def self.reloadTheme : Theme?
      Benben.dlog!("Attempting to reload theme config for '#{Benben.theme.name}'")
      newTheme = Theme.load(Benben.resolver, Benben.theme.name)
      Benben.dlog!("Loaded theme config for '#{Benben.theme.name}': #{newTheme}")
      Benben.theme = newTheme
      Benben.dlog!("Theme configuration reloaded and set, global theme is now #{Benben.theme}")
      newTheme
    rescue err : Exception
      RemiLib.log.error("Could not reload theme: #{err}")
      Benben.ui.queueError("Could not reload theme: #{err}")
      nil
    end
  end
end
Changes to src/main.cr.
57
58
59
60
61
62
63



64











65
66
67
68
69
70
71
  RemiLib.classSetonce! logFile : File?

  # The loaded configuration file.
  @@config : EphemeralConfig = EphemeralConfig.new
  class_property config

  # The loaded UI theme.



  RemiLib.classSetonce! theme : Theme?












  # Used to find the configuration file and data files.
  @@resolver = RemiConf::Resolver.xdg(SHORT_NAME)
  class_getter resolver

  # Holds the original screen attributes.
  @@originalAttrs : LibC::Termios?







>
>
>
|
>
>
>
>
>
>
>
>
>
>
>







57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
  RemiLib.classSetonce! logFile : File?

  # The loaded configuration file.
  @@config : EphemeralConfig = EphemeralConfig.new
  class_property config

  # The loaded UI theme.
  @@theme : Atomic(Theme?) = Atomic(Theme?).new(nil)
  #@@themeMut : Mutex = Mutex.new

  def self.theme : Theme
    #@@themeMut.synchronize { @@theme.not_nil! }
    @@theme.get.not_nil!
  end

  def self.theme=(newTheme : Theme) : Theme
    #@@themeMut.synchronize { @@theme = newTheme }
    @@theme.set(newTheme)
    newTheme
  end

  #class_property! theme

  # Used to find the configuration file and data files.
  @@resolver = RemiConf::Resolver.xdg(SHORT_NAME)
  class_getter resolver

  # Holds the original screen attributes.
  @@originalAttrs : LibC::Termios?
Changes to src/uis/orig/banner.cr.
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
      # Initializes offsets for the banner lines.  This must be called before
      # any animation is performed.
      abstract def initOffsets : Nil

      # Refreshes the border lines.
      def refreshBorderLines(force : Bool = false) : Nil
        if force
          color = OrigUI.theme.maxBannerLineColor
          #SLScreen.goto(0, 0)
          SLScreen.drawHline(0, 0, color: color)
          SLScreen.drawHline(0, @bottomBorderY, color: color)
        else
          # See if it's time to animate the border lines.
          if @borderLinesTimeout < Time.local
            # The animation hasn't finished.  First update the lines that go
            # above/below the banner.  We go through all banner line colors
            # twice, hence the *2 and %2 below.
            if @doBorderColorChange == 0 && @borderColorOff < OrigUI.theme.numBannerLineColors * 2
              color = (Theme::BANNER_LINES_START + (@borderColorOff % OrigUI.theme.numBannerLineColors))
              SLScreen.drawHline(0, 0, color: color)
              SLScreen.drawHline(0, @bottomBorderY, color: color)
              @borderColorOff += 1
            end
            @doBorderColorChange = (@doBorderColorChange + 1) % 2 # Every third frame

            # Did we finish the entire loop?
            if @borderColorOff == OrigUI.theme.numBannerLineColors * 2
              # Reset, wait for next redraw
              @borderLinesTimeout = Time.local + BANNER_LINES_TIMEOUT
              @doBorderColorChange = 0
              @borderColorOff = 0
            end
          end
        end







|
<

















|







54
55
56
57
58
59
60
61

62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
      # Initializes offsets for the banner lines.  This must be called before
      # any animation is performed.
      abstract def initOffsets : Nil

      # Refreshes the border lines.
      def refreshBorderLines(force : Bool = false) : Nil
        if force
          color = OrigUI.theme.maxBannerLineColor - 1

          SLScreen.drawHline(0, 0, color: color)
          SLScreen.drawHline(0, @bottomBorderY, color: color)
        else
          # See if it's time to animate the border lines.
          if @borderLinesTimeout < Time.local
            # The animation hasn't finished.  First update the lines that go
            # above/below the banner.  We go through all banner line colors
            # twice, hence the *2 and %2 below.
            if @doBorderColorChange == 0 && @borderColorOff < OrigUI.theme.numBannerLineColors * 2
              color = (Theme::BANNER_LINES_START + (@borderColorOff % OrigUI.theme.numBannerLineColors))
              SLScreen.drawHline(0, 0, color: color)
              SLScreen.drawHline(0, @bottomBorderY, color: color)
              @borderColorOff += 1
            end
            @doBorderColorChange = (@doBorderColorChange + 1) % 2 # Every third frame

            # Did we finish the entire loop?
            if @borderColorOff >= OrigUI.theme.numBannerLineColors * 2
              # Reset, wait for next redraw
              @borderLinesTimeout = Time.local + BANNER_LINES_TIMEOUT
              @doBorderColorChange = 0
              @borderColorOff = 0
            end
          end
        end
Changes to src/uis/orig/orig.cr.
26
27
28
29
30
31
32







33



34
35
36
37
38
39
40

module Benben
  alias Slang = RemiSlang
  alias SLScreen = RemiSlang::Screen
  alias SLInput = RemiSlang::Input

  class OrigUI < UI







    property currentAttrs : LibC::Termios?




    # How often the CPU usage is refreshed, in milliseconds.
    CPU_USAGE_UPDATE_FREQ = 1000

    # How long a message is displayed.
    MESSAGE_TIME = 5.seconds








>
>
>
>
>
>
>
|
>
>
>







26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

module Benben
  alias Slang = RemiSlang
  alias SLScreen = RemiSlang::Screen
  alias SLInput = RemiSlang::Input

  class OrigUI < UI
    enum Mode
      Playing
      Paused
      PlayingStopAfterCurrent
      PausedStopAfterCurrent
      Undefined
    end

    ##
    ## Constants
    ##

    # How often the CPU usage is refreshed, in milliseconds.
    CPU_USAGE_UPDATE_FREQ = 1000

    # How long a message is displayed.
    MESSAGE_TIME = 5.seconds

64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82



83



84





85



86


87
88
89
90
91
92
93
    HELP_MESSAGE_MIDI = %|
n - Next  p - Prev  x - Toggle Chorus     a - Vol up
q - Quit  e - EQ    c - Toggle soft clip  z - Vol down
s - Toggle stereo enhancer                Space - Pause/Unpause
R - Redraw screen   S - Stop after song   C - Reload song configs
    |.strip.lines

    enum Mode
      Playing
      Paused
      PlayingStopAfterCurrent
      PausedStopAfterCurrent
      Undefined
    end

    ##
    ## Theme
    ##




    @@theme : Theme?



    class_getter! theme









    INFO_LINE_HEADER = "Info: "



    # Mutex for thread-sensitive UI code.
    getter uiLock : Mutex = Mutex.new

    # Screen info
    getter lastWidth : Int32 = 0
    getter lastHeight : Int32 = 0







|
<
<
<
<
<
<





>
>
>
|
>
>
>
|
>
>
>
>
>

>
>
>
|
>
>







74
75
76
77
78
79
80
81






82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
    HELP_MESSAGE_MIDI = %|
n - Next  p - Prev  x - Toggle Chorus     a - Vol up
q - Quit  e - EQ    c - Toggle soft clip  z - Vol down
s - Toggle stereo enhancer                Space - Pause/Unpause
R - Redraw screen   S - Stop after song   C - Reload song configs
    |.strip.lines

    INFO_LINE_HEADER = "Info: "







    ##
    ## Theme
    ##

    @@theme : Atomic(Theme?) = Atomic(Theme?).new(nil)
    #@@themeMut : Mutex = Mutex.new

    def self.theme : Theme
      @@theme.get.not_nil!
      #@@themeMut.synchronize { @@theme.not_nil! }
    end

    def self.theme=(newTheme : Theme) : Theme
      @@theme.set(newTheme)
      #@@themeMut.synchronize { @@theme = newTheme }
      newTheme
    end

    ##
    ## Fields
    ##

    # Current terminal attributes.
    property currentAttrs : LibC::Termios?

    # Mutex for thread-sensitive UI code.
    getter uiLock : Mutex = Mutex.new

    # Screen info
    getter lastWidth : Int32 = 0
    getter lastHeight : Int32 = 0
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151

    # Message handling
    @messageTimeout : Time?
    @errorQueue : Deque(String) = Deque(String).new(1)
    @messageQueue : Deque(String) = Deque(String).new(1)

    def initialize
      @@theme = Theme.new(Benben.theme)
      @animations = Benben.config.uiConfig.animationsEnabled?

      @banner = Banner.getBanner(@animations)
      @mainAreaStartRow = @banner.size

      @tagDisplay = TagDisplay.new(@mainAreaStartRow)
      @playQueue = PlayQueue.new(@mainAreaStartRow + 8)







|







157
158
159
160
161
162
163
164
165
166
167
168
169
170
171

    # Message handling
    @messageTimeout : Time?
    @errorQueue : Deque(String) = Deque(String).new(1)
    @messageQueue : Deque(String) = Deque(String).new(1)

    def initialize
      OrigUI.theme = Theme.new(Benben.theme)
      @animations = Benben.config.uiConfig.animationsEnabled?

      @banner = Banner.getBanner(@animations)
      @mainAreaStartRow = @banner.size

      @tagDisplay = TagDisplay.new(@mainAreaStartRow)
      @playQueue = PlayQueue.new(@mainAreaStartRow + 8)
528
529
530
531
532
533
534

535
536
537
538
539
540
541
        when 'S' then Benben.manager.sendToAllKeyChans(KeyCommand::StopAfterCurrent)
        when 'C' then Benben.manager.sendToAllKeyChans(KeyCommand::ReloadSongConfigs)
        when 'i' then Benben.manager.sendToAllKeyChans(KeyCommand::ToggleInterpolation)
        when 'x' then Benben.manager.sendToAllKeyChans(KeyCommand::ToggleChorus)
        when 'R' then Benben.manager.sendToAllKeyChans(KeyCommand::ForceRedraw)
        when 'q' then Benben.manager.sendToAllKeyChans(KeyCommand::Exit)
        when 'h' then Benben.manager.sendToAllKeyChans(KeyCommand::Help)


        {% if flag?(:benben_manual_gc) %}
        when 'G' then GC.collect
        {% end %}
        end
      {% end %}
    end







>







548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
        when 'S' then Benben.manager.sendToAllKeyChans(KeyCommand::StopAfterCurrent)
        when 'C' then Benben.manager.sendToAllKeyChans(KeyCommand::ReloadSongConfigs)
        when 'i' then Benben.manager.sendToAllKeyChans(KeyCommand::ToggleInterpolation)
        when 'x' then Benben.manager.sendToAllKeyChans(KeyCommand::ToggleChorus)
        when 'R' then Benben.manager.sendToAllKeyChans(KeyCommand::ForceRedraw)
        when 'q' then Benben.manager.sendToAllKeyChans(KeyCommand::Exit)
        when 'h' then Benben.manager.sendToAllKeyChans(KeyCommand::Help)
        when 'T' then Benben.manager.sendToAllKeyChans(KeyCommand::ReloadTheme)

        {% if flag?(:benben_manual_gc) %}
        when 'G' then GC.collect
        {% end %}
        end
      {% end %}
    end
632
633
634
635
636
637
638











639
640
641
642
643
644
645
            @mainVolume = newVol
            vu.resetClips
            updateVolume

          when .reload_song_configs?
            queueMessage("Reloading song configs")
            Benben.config.loadSongConfigs












          when .force_redraw?
            Benben.dlog!("Redrawing entire screen due to request")
            redrawWholeScreen

          when .stop_after_current?
            @stopAfterCurrent = !@stopAfterCurrent







>
>
>
>
>
>
>
>
>
>
>







653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
            @mainVolume = newVol
            vu.resetClips
            updateVolume

          when .reload_song_configs?
            queueMessage("Reloading song configs")
            Benben.config.loadSongConfigs

          when .reload_theme?
            Benben.dlog!("UI is attempting to reload the theme")
            if newTheme = Benben::Theme.reloadTheme
              Benben.dlog!("Same?  #{newTheme} == #{Benben.theme}")
              OrigUI.theme = Theme.new(newTheme)
              initColors
              initVu
              queueRedrawWholeScreen
              queueMessage("Theme reloaded")
            end

          when .force_redraw?
            Benben.dlog!("Redrawing entire screen due to request")
            redrawWholeScreen

          when .stop_after_current?
            @stopAfterCurrent = !@stopAfterCurrent
Changes to src/uis/orig/theme.cr.
104
105
106
107
108
109
110










111
112
113
114
115
116
117
        @bgColor = getColorStr(@config.bgColor)
        @fgColor = getColorStr(@config.fgColor)
        @bannerColor = getColorStr(@config.bannerColor)
        @pbarChar = @config.progressBarChar
        @pbarSpaceChar = @config.progressBarSpaceChar
        @maxBannerLineColor = BANNER_LINES_START + @config.bannerLines.size
      end











      # Converts *color* into a color string recognized by S-Lang.
      private def getColorStr(color : Benben::ThemeColor) : String
        case color
        in UInt8 then "color#{color}"
        in Tuple(UInt8, UInt8, UInt8)
          sprintf("#%02X%02X%02X", color[0], color[1], color[2])







>
>
>
>
>
>
>
>
>
>







104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
        @bgColor = getColorStr(@config.bgColor)
        @fgColor = getColorStr(@config.fgColor)
        @bannerColor = getColorStr(@config.bannerColor)
        @pbarChar = @config.progressBarChar
        @pbarSpaceChar = @config.progressBarSpaceChar
        @maxBannerLineColor = BANNER_LINES_START + @config.bannerLines.size
      end

      private def printDebugThemeInfo : Nil
        Benben.dlog!("UI Theme is from:      #{@config}")
        Benben.dlog!("UI Theme bg color:     #{@bgColor} (#{@config.bgColor})")
        Benben.dlog!("UI Theme fg color:     #{@fgColor} (#{@config.fgColor})")
        Benben.dlog!("UI Theme banner color: #{@bannerColor} (#{@config.bannerColor})")
        Benben.dlog!("UI Theme header color: #{getColorStr(@config.headerColor)} (#{@config.headerColor})")
        Benben.dlog!("UI Theme error color:  #{getColorStr(@config.errColor)} (#{@config.errColor})")
        Benben.dlog!("UI Theme song color:   #{getColorStr(@config.curSongColor)} (#{@config.curSongColor})")
      end

      # Converts *color* into a color string recognized by S-Lang.
      private def getColorStr(color : Benben::ThemeColor) : String
        case color
        in UInt8 then "color#{color}"
        in Tuple(UInt8, UInt8, UInt8)
          sprintf("#%02X%02X%02X", color[0], color[1], color[2])
155
156
157
158
159
160
161


162
163
164
165
166
167
168

        @pbarColors = [] of Int32
        @config.progressBarColors.each do |col|
          @pbarColors << (PBAR_COLOR_START + @pbarColors.size)
          SLScreen.defineColor(@pbarColors[-1], getColorStr(col), @bgColor)
        end
        @maxPbarColorIdx = (@pbarColors.size - 1).to_f32!


      end

      # Resets the progress bar color to the lowest color.
      def resetBarColor : Nil
        @curBarColor = 0.0f32
      end








>
>







165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180

        @pbarColors = [] of Int32
        @config.progressBarColors.each do |col|
          @pbarColors << (PBAR_COLOR_START + @pbarColors.size)
          SLScreen.defineColor(@pbarColors[-1], getColorStr(col), @bgColor)
        end
        @maxPbarColorIdx = (@pbarColors.size - 1).to_f32!

        printDebugThemeInfo
      end

      # Resets the progress bar color to the lowest color.
      def resetBarColor : Nil
        @curBarColor = 0.0f32
      end