yast/yast-yast2

View on GitHub
library/desktop/src/modules/Desktop.rb

Summary

Maintainability
C
1 day
Test Coverage
# ***************************************************************************
#
# Copyright (c) 2002 - 2012 Novell, Inc.
# All Rights Reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public License as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.   See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, contact Novell, Inc.
#
# To contact Novell about this file by physical or electronic mail,
# you may find current contact information at www.novell.com
#
# ***************************************************************************
# File:  modules/Desktop.ycp
# Package:  yast2
# Summary:  Handling of .desktop entries
# Author:  Michal Svec <msvec@suse.cz>
#
# $Id$
require "yast"

module Yast
  class DesktopClass < Module
    def main
      textdomain "base"
      Yast.import "Arch"
      Yast.import "Map"
      Yast.import "Directory"

      # YaST configuration modules
      @Modules = {}

      #  * YaST configuration groups
      #  *
      #  * <PRE>
      #     Groups=$[
      #   "Hardware":$[
      #       "Icon":"hardware56.png",
      #       "Name":"_(\"Hardware\")",
      #       "SortKey":"20",
      #       "Textdomain":"base",
      #       "modules":["cdrom", "hwinfo", ...]
      #   ],
      #   ...
      #    ];
      #  * </PRE>
      @Groups = {}

      # Optional agent path to the desktop files
      @AgentPath = path(".yast2.desktop")

      # Optional language for reading translated entries
      @Language = ""
      @LanguageFull = ""
    end

    def ReadLocalizedKey(fname, keypath, key)
      return Convert.to_string(SCR.Read(Builtins.add(keypath, key))) if key != "Name" && key != "GenericName" && key != "Comment"

      ret = ""
      fallback = Convert.to_string(SCR.Read(Builtins.add(keypath, key)))

      # check if there are any translation in .desktop file
      # that is - Name[$lang_code]
      if !@LanguageFull.nil? || @LanguageFull != ""
        newkey = Builtins.sformat("%1[%2]", key, @LanguageFull)
        ret = Convert.to_string(SCR.Read(Builtins.add(keypath, newkey)))
        return ret if !ret.nil? && ret != ""
      end

      if !@Language.nil? || @Language != ""
        newkey = Builtins.sformat("%1[%2]", key, @Language)
        ret = Convert.to_string(SCR.Read(Builtins.add(keypath, newkey)))
        return ret if !ret.nil? && ret != ""
      end

      # no translations in .desktop, check desktop_translations.mo then
      msgid = Builtins.sformat("%1(%2): %3", key, fname, fallback)
      Builtins.y2debug("Looking for key: %1", msgid)
      ret = Builtins.dpgettext(
        "desktop_translations",
        "/usr/share/locale",
        msgid
      )

      # probably untranslated - return english name
      return fallback if ret == msgid

      ret
    end

    # Internal function: set up the language variables.
    def ReadLanguage
      # read language
      @LanguageFull = ""
      @Language = ""

      # Do not use UI.GetLanguage. It would start an UI which is not
      # needed for it.
      @Language = ENV["LANG"].split(/[.,@]/).first if ENV["LANG"]

      @LanguageFull = Builtins.regexpsub(@Language, "(.*_[^.]*)\\.?.*", "\\1") if Builtins.regexpmatch(@Language, "(.*_[^.]*)\\.?.*") # matches: ll_TT ll_TT.UTF-8
      @Language = Builtins.regexpsub(@Language, "(.*)_", "\\1") if Builtins.regexpmatch(@Language, "(.*)_")
      Builtins.y2debug("LanguageFull=%1", @LanguageFull)
      Builtins.y2debug("Language=%1", @Language)

      nil
    end

    # Read module and group data from desktop files
    # @param [Array<String>] Values list of values to be parsed (empty or nil reads nothing)
    def Read(values_to_parse)
      values_to_parse = deep_copy(values_to_parse)
      extract_desktop_filename = lambda do |fullpath|
        path_components = Builtins.splitstring(fullpath, "/")
        filename = Ops.get(
          path_components,
          Ops.subtract(Builtins.size(path_components), 1),
          ""
        )

        filename
      end

      # read modules
      filemap = {}
      filepath = nil

      ReadLanguage()

      ps = Builtins.add(@AgentPath, "s")
      files = SCR.Dir(ps)

      # read groups
      groups = SCR.Dir(path(".yast2.groups.s"))
      Builtins.y2debug("groups=%1", groups)
      Builtins.foreach(groups) do |group|
        filemap = {}
        filepath = Ops.add(
          Ops.add(path(".yast2.groups.v"), group),
          "Desktop Entry"
        )
        filename = extract_desktop_filename.call(group)
        Ops.set(filemap, "Icon", SCR.Read(Ops.add(filepath, "Icon")))
        Ops.set(
          filemap,
          "SortKey",
          SCR.Read(Ops.add(filepath, "X-SuSE-YaST-SortKey"))
        )
        Ops.set(filemap, "Hidden", SCR.Read(Ops.add(filepath, "Hidden")))
        Ops.set(filemap, "Name", ReadLocalizedKey(filename, filepath, "Name"))
        Ops.set(filemap, "GenericName", ReadLocalizedKey(filename, filepath, "GenericName"))
        Ops.set(filemap, "modules", [])
        Ops.set(
          filemap,
          "X-SuSE-DocTeamID",
          SCR.Read(Ops.add(filepath, "X-SuSE-DocTeamID"))
        )
        name2 = Convert.to_string(
          SCR.Read(Ops.add(filepath, "X-SuSE-YaST-Group"))
        )
        Ops.set(@Groups, name2, filemap)
      end
      Builtins.y2debug("Groups=%1", @Groups)

      # read modules
      Builtins.foreach(files) do |file|
        filemap = {}
        filepath = Ops.add(
          Ops.add(Ops.add(@AgentPath, path(".v")), file),
          path(".\"Desktop Entry\"")
        )
        values = SCR.Dir(filepath)
        filename = extract_desktop_filename.call(file)
        values = deep_copy(values_to_parse) if !values_to_parse.nil? && values_to_parse != []
        Builtins.foreach(values) do |value|
          ret = ReadLocalizedKey(filename, filepath, value)
          Ops.set(filemap, value, ret) if !ret.nil? && ret != ""
        end
        name2 = desktop_file_to_module(file)
        if name2 != "" && !name2.nil?
          Ops.set(@Modules, name2, filemap)
          group = Ops.get_string(filemap, "X-SuSE-YaST-Group", "")
          if group != ""
            Ops.set(
              @Groups,
              [
                group,
                "modules",
                Builtins.size(Ops.get_list(@Groups, [group, "modules"], []))
              ],
              name2
            )
          end
        end
      end
      Builtins.y2debug("Groups=%1", @Groups)
      Builtins.y2debug("Modules=%1", @Modules)

      nil
    end

    def Translate(key)
      if Builtins.regexpmatch(key, "_\\(\"(.*)\"\\)") == true
        ke = Builtins.regexpsub(key, "_\\(\"(.*)\"\\)", "\\1")
        key = Builtins.eval(ke)
        Builtins.y2milestone("%1 -> %2", ke, key)
      end
      key
    end

    def CreateList(entry)
      entry = deep_copy(entry)
      keys = Map.Keys(entry)
      keys = Builtins.sort(keys) do |x, y|
        Ops.less_than(
          Ops.get_string(entry, [x, "SortKey"], ""),
          Ops.get_string(entry, [y, "SortKey"], "")
        )
      end

      keys = Builtins.filter(keys) do |key|
        Ops.get_string(entry, [key, "Hidden"], "false") != "true"
      end

      Builtins.y2debug("keys=%1", keys)

      Builtins.maplist(keys) do |name|
        Item(Id(name), Translate(Ops.get_string(entry, [name, "Name"], "???")))
      end
    end

    def GroupList
      CreateList(@Groups)
    end

    def ModuleList(group)
      mods = Ops.get_list(@Groups, [group, "modules"], [])
      l = []

      # support sort keys: #36466
      mods = Builtins.sort(mods) do |x, y|
        Ops.less_than(
          Ops.get_string(
            @Modules,
            [x, "X-SuSE-YaST-SortKey"],
            Ops.get_string(@Modules, [x, "GenericName"], "")
          ),
          Ops.get_string(
            @Modules,
            [y, "X-SuSE-YaST-SortKey"],
            Ops.get_string(@Modules, [y, "GenericName"], "")
          )
        )
      end

      Builtins.foreach(mods) do |m|
        if @Modules[m].is_a?(::Hash) &&
            @Modules[m]["Hidden"] != "true" &&
            # wsl specific whitelisting of modules
            (!Arch.is_wsl || @Modules[m]["X-SuSE-YaST-WSL"] == "true")
          l << Item(Id(m), Ops.get_string(@Modules, [m, "GenericName"], "???"))
        end
      end

      # y2debug too costly: y2debug("%1", m);
      deep_copy(l)
    end

    def MakeAutostartMap(exec, args)
      args = deep_copy(args)
      {
        "Encoding"         => "UTF-8",
        "Name"             => exec,
        "Exec"             => exec,
        "X-SuSE-Autostart" => Ops.add(
          Ops.add(exec, " "),
          Builtins.mergestring(args, " ")
        ),
        "Hidden"           => "true",
        "Icon"             => exec,
        "Type"             => "Application"
      }
    end

    # Runs a program by writing a special desktop file.
    # Works with KDE and GNOME.
    # Useful for kinternet, see bug 37864#c17
    # @param [String] exec program to exec (basename)
    def RunViaDesktop(exec, args)
      args = deep_copy(args)
      content = "[KDE Desktop Entry]\n"
      Builtins.foreach(MakeAutostartMap(exec, args)) do |key, value|
        content = Ops.add(content, Builtins.sformat("%1=%2\n", key, value))
      end
      dir = "/var/lib/Desktop"
      SCR.Write(
        path(".target.string"),
        Builtins.sformat("%1/yast2-run-%2.desktop", dir, exec),
        content
      )

      nil
    end

    # Parses the a .desktop file it gets as a parameter without trying to use
    # already cached information or agent to access all desktop files. This is
    # optimized version to be used for rapid start of modules.
    # Desktop file is placed in a special directory (/usr/share/applications/YaST2).
    # Parameter file is relative to that directory without ".desktop" suffix.
    # Warning: There are no desktop files in inst-sys.
    #
    # @param [String] file desktop file name
    # @return [Hash] filled with data, or nil
    #
    # @example
    #  // Opens /usr/share/applications/YaST2/lan.desktop
    #  map<string,string> description = Desktop::ParseSingleDesktopFile ("lan");
    #  Wizard::SetDialogTitle (description["Name"]:_("None));
    def ParseSingleDesktopFile(file)
      filename = Builtins.sformat("%1/%2.desktop", Directory.desktopdir, file)
      # Do not use .yast2.desktop.v.$filename, because ini-agent reads
      # all the desktop files anyway which is wasteful for setting one icon.
      # The config is adapted from .yast2.desktop.
      SCR.RegisterAgent(
        path(".yast2.desktop1"),
        term(
          :ag_ini,
          term(
            :IniAgent,
            filename,
            "options"  => ["read_only"], # rw works but not needed
            "comments" => ["^[ \t]*[;#].*", ";.*", "\\{[^}]*\\}", "^[ \t]*$"],
            "sections" => [
              {
                "begin" => [
                  "^[ \t]*\\[[ \t]*(.*[^ \t])[ \t]*\\][ \t]*",
                  "[%s]"
                ]
              }
            ],
            "params"   => [
              {
                "match" => [
                  "^[ \t]*([^=]*[^ \t=])[ \t]*=[ \t]*(.*[^ \t]|)[ \t]*$",
                  "%s=%s"
                ]
              }
            ]
          )
        )
      )

      # non-existent file requested
      if SCR.Dir(path(".yast2.desktop1.v.\"Desktop Entry\"")).nil?
        Builtins.y2error("Unknown desktop file: %1", file)
        SCR.UnregisterAgent(path(".yast2.desktop1"))
        return nil
      end

      # we need localized keys
      ReadLanguage()

      result = {
        "Icon"        => Convert.to_string(
          SCR.Read(path(".yast2.desktop1.v.\"Desktop Entry\".Icon"))
        ),
        "Name"        => ReadLocalizedKey(
          Ops.add(file, ".desktop"),
          path(".yast2.desktop1.v.\"Desktop Entry\""),
          "Name"
        ),
        "GenericName" => ReadLocalizedKey(
          Ops.add(file, ".desktop"),
          path(".yast2.desktop1.v.\"Desktop Entry\""),
          "GenericName"
        ),
        "Comment"     => ReadLocalizedKey(
          Ops.add(file, ".desktop"),
          path(".yast2.desktop1.v.\"Desktop Entry\""),
          "Comment"
        )
      }

      SCR.UnregisterAgent(path(".yast2.desktop1"))

      deep_copy(result)
    end

    publish variable: :Modules, type: "map <string, map>"
    publish variable: :Groups, type: "map <string, map>"
    publish variable: :AgentPath, type: "path"
    publish function: :Read, type: "void (list <string>)"
    publish function: :Translate, type: "string (string)"
    publish function: :GroupList, type: "list <term> ()"
    publish function: :ModuleList, type: "list <term> (string)"
    publish function: :RunViaDesktop, type: "void (string, list <string>)"
    publish function: :ParseSingleDesktopFile, type: "map <string, string> (string)"

  private

    # @return [Regexp] Regular expression which matches the module name of a desktop file
    FILE_REGEXP = /([^.]+).desktop\Z/.freeze

    # Converts the file name to the expected module name
    #
    # In 2019, desktop files were renamed to org.opensuse.yast.MODULE.desktop. This method extracts
    # and normalizes the *MODULE* part of the filename.
    #
    # @param [String] filename
    # @return [String,nil] Module name or nil if it cannot be inferred.
    # @see https://github.com/yast/yast-yast2/issues/934
    def desktop_file_to_module(filename)
      basename = File.basename(filename)
      name = basename[FILE_REGEXP, 1]
      if name.nil?
        log.info "Could not infer the module name from #{filename}"
        return
      end

      # E.g. 'AddOn' to 'add-on', 'DNSServer' to 'dns-server' and so on.
      name
        .gsub(/([[:upper:]\d]*)([[:upper:]\d])([[:lower:]]+)/, '\1-\2\3')
        .sub(/\A-/, "")
        .downcase
    end
  end

  Desktop = DesktopClass.new
  Desktop.main
end