1 //===========================================
2 //  Lumina-DE source code
3 //  Copyright (c) 2013-2016, Ken Moore
4 //  Available under the 3-clause BSD license
5 //  See the LICENSE file for full details
6 //===========================================
7 #include "LUtils.h"
8 
9 #include "LuminaOS.h"
10 #include "LuminaXDG.h"
11 
12 #include <QApplication>
13 #include <QtConcurrent>
14 
15 #include <unistd.h>
16 
17 /*inline QStringList ProcessRun(QString cmd, QStringList args){
18   //Assemble outputs
19   QStringList out; out << "1" << ""; //error code, string output
20   QProcess proc;
21   QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
22   env.insert("LANG", "C");
23   env.insert("LC_MESSAGES", "C");
24   proc.setProcessEnvironment(env);
25   proc.setProcessChannelMode(QProcess::MergedChannels);
26   if(args.isEmpty()){
27     proc.start(cmd, QIODevice::ReadOnly);
28   }else{
29     proc.start(cmd,args ,QIODevice::ReadOnly);
30   }
31   QString info;
32   while(!proc.waitForFinished(1000)){
33     if(proc.state() == QProcess::NotRunning){ break; } //somehow missed the finished signal
34     QString tmp = proc.readAllStandardOutput();
35     if(tmp.isEmpty()){ proc.terminate(); }
36     else{ info.append(tmp); }
37   }
38   out[0] = QString::number(proc.exitCode());
39   out[1] = info+QString(proc.readAllStandardOutput());
40   return out;
41 }*/
42 
43 //=============
44 //  LUtils Functions
45 //=============
46 
47 //Return the path to one of the XDG standard directories
standardDirectory(StandardDir dir,bool createAsNeeded)48 QString LUtils::standardDirectory(StandardDir dir, bool createAsNeeded){
49 // enum StandardDir {Desktop, Documents, Downloads, Music, Pictures, PublicShare, Templates, Videos}
50   QString var="XDG_%1_DIR";
51   QString defval="$HOME";
52   QString val;
53   switch (dir){
54     case Desktop:
55       var = var.arg("DESKTOP");
56       defval.append("/Desktop");
57       break;
58     case Documents:
59       var = var.arg("DOCUMENTS");
60       defval.append("/Documents");
61       break;
62     case Downloads:
63       var = var.arg("DOWNLOAD");
64       defval.append("/Downloads");
65       break;
66     case Music:
67       var = var.arg("MUSIC");
68       defval.append("/Music");
69       break;
70     case Pictures:
71       var = var.arg("PICTURES");
72       defval.append("/Pictures");
73       break;
74     case PublicShare:
75       var = var.arg("PUBLICSHARE");
76       break;
77     case Templates:
78       var = var.arg("TEMPLATES");
79       break;
80     case Videos:
81       var = var.arg("VIDEOS");
82       defval.append("/Videos");
83       break;
84   }
85   //Read the XDG user dirs file (if it exists)
86   QString configdir = getenv("XDG_DATA_HOME");
87   if(configdir.isEmpty()){ configdir = QDir::homePath()+"/.config"; }
88   QString conffile=configdir+"/user-dirs.dirs";
89   if(QFile::exists(conffile)){
90     static QStringList _contents;
91     static QDateTime _lastread;
92     if(_contents.isEmpty() || _lastread < QFileInfo(conffile).lastModified()){
93       _contents = LUtils::readFile(conffile);
94       _lastread = QDateTime::currentDateTime();
95     }
96     QStringList match = _contents.filter(var+"=");
97     if(!match.isEmpty()){
98       val = match.first().section("=",-1).simplified();
99       if(val.startsWith("\"")){ val = val.remove(0,1); }
100       if(val.endsWith("\"")){ val.chop(1); }
101     }
102   }
103   //Now check the value and return it
104   if(val.isEmpty()){ val = defval; }; //use the default value
105   val = val.replace("$HOME", QDir::homePath());
106   if(createAsNeeded && !QFile::exists(val) ){
107     QDir dir;
108     dir.mkpath(val);
109   }
110   return val;
111 }
112 
runCommand(bool & success,QString command,QStringList arguments,QString workdir,QStringList env)113 QString LUtils::runCommand(bool &success, QString command, QStringList arguments, QString workdir, QStringList env){
114   QProcess proc;
115     proc.setProcessChannelMode(QProcess::MergedChannels); //need output
116   //First setup the process environment as necessary
117   QProcessEnvironment PE = QProcessEnvironment::systemEnvironment();
118     if(!env.isEmpty()){
119       for(int i=0; i<env.length(); i++){
120     if(!env[i].contains("=")){ continue; }
121         PE.insert(env[i].section("=",0,0), env[i].section("=",1,100));
122       }
123     }
124     proc.setProcessEnvironment(PE);
125   //if a working directory is specified, check it and use it
126   if(!workdir.isEmpty()){
127     proc.setWorkingDirectory(workdir);
128   }
129   //Now run the command (with any optional arguments)
130   if(arguments.isEmpty()){ proc.start(command); }
131   else{ proc.start(command, arguments); }
132   //Wait for the process to finish (but don't block the event loop)
133   QString info;
134   while(!proc.waitForFinished(1000)){
135     if(proc.state() == QProcess::NotRunning){ break; } //somehow missed the finished signal
136     QString tmp = proc.readAllStandardOutput();
137     if(tmp.isEmpty()){ proc.terminate(); }
138     else{ info.append(tmp); }
139   }
140   info.append(proc.readAllStandardOutput()); //make sure we don't miss anything in the output
141   success = (proc.exitCode()==0); //return success/failure
142   return info;
143 }
144 
runCmd(QString cmd,QStringList args)145 int LUtils::runCmd(QString cmd, QStringList args){
146   bool success;
147   LUtils::runCommand(success, cmd, args);
148   return success;
149 
150   /*QFuture<QStringList> future = QtConcurrent::run(ProcessRun, cmd, args);
151   return future.result()[0].toInt(); //turn it back into an integer return code*/
152 }
153 
getCmdOutput(QString cmd,QStringList args)154 QStringList LUtils::getCmdOutput(QString cmd, QStringList args){
155   bool success;
156   QString log = LUtils::runCommand(success, cmd, args);
157   return log.split("\n");
158   /*QFuture<QStringList> future = QtConcurrent::run(ProcessRun, cmd, args);
159   return future.result()[1].split("\n"); //Split the return message into lines*/
160 }
161 
readFile(QString filepath)162 QStringList LUtils::readFile(QString filepath){
163   QStringList out;
164   QFile file(filepath);
165   if(file.open(QIODevice::Text | QIODevice::ReadOnly)){
166     QTextStream in(&file);
167     while(!in.atEnd()){
168       out << in.readLine();
169     }
170     file.close();
171   }
172   return out;
173 }
174 
writeFile(QString filepath,QStringList contents,bool overwrite)175 bool LUtils::writeFile(QString filepath, QStringList contents, bool overwrite){
176   QFile file(filepath);
177   if(file.exists() && !overwrite){ return false; }
178   bool ok = false;
179   if(contents.isEmpty()){ contents << "\n"; }
180   if( file.open(QIODevice::WriteOnly | QIODevice::Truncate) ){
181     QTextStream out(&file);
182     out << contents.join("\n");
183     if(!contents.last().isEmpty()){ out << "\n"; } //always end with a new line
184     file.close();
185     ok = true;
186   }
187   return ok;
188 }
189 
isValidBinary(QString & bin)190 bool LUtils::isValidBinary(QString& bin){
191   //Trim off any quotes
192   if(bin.startsWith("\"") && bin.endsWith("\"")){ bin.chop(1); bin = bin.remove(0,1); }
193   if(bin.startsWith("\'") && bin.endsWith("\'")){ bin.chop(1); bin = bin.remove(0,1); }
194   //Now look for relative/absolute path
195   if(!bin.startsWith("/")){
196     //Relative path: search for it on the current "PATH" settings
197     QStringList paths = QString(qgetenv("PATH")).split(":");
198     for(int i=0; i<paths.length(); i++){
199       if(QFile::exists(paths[i]+"/"+bin)){ bin = paths[i]+"/"+bin; break;}
200     }
201   }
202   //bin should be the full path by now
203   if(!bin.startsWith("/")){ return false; }
204   QFileInfo info(bin);
205   bool good = (info.exists() && info.isExecutable());
206   if(good){ bin = info.absoluteFilePath(); }
207   return good;
208 }
209 
openSettings(QString org,QString name,QObject * parent)210 QSettings* LUtils::openSettings(QString org, QString name, QObject *parent){
211   //Start with the base configuration directory
212   QString path = QString(getenv("XDG_CONFIG_HOME")).simplified();
213   if(path.isEmpty()){ path = QDir::homePath()+"/.config"; }
214   //Now add the organization directory
215   path = path+"/"+org;
216   QDir dir(path);
217   if(!dir.exists()){ dir.mkpath(path); }
218   //Now generate/check the name of the file
219   unsigned int user = getuid();
220   QString filepath = dir.absoluteFilePath(name+".conf");
221   if(user==0){
222     //special case - make sure we don't clobber the user-permissioned file
223     QString rootfilepath = dir.absoluteFilePath(name+"_root.conf");
224     if(!QFileInfo::exists(rootfilepath) && QFileInfo::exists(filepath)){
225       QFile::copy(filepath, rootfilepath); //make a copy of the user settings before they start to diverge
226     }
227     return (new QSettings(rootfilepath, QSettings::IniFormat, parent));
228   }else{
229     return (new QSettings(filepath, QSettings::IniFormat, parent));
230   }
231 
232 }
233 
systemApplicationDirs()234 QStringList LUtils::systemApplicationDirs(){
235   //Returns a list of all the directories where *.desktop files can be found
236   QStringList appDirs = QString(getenv("XDG_DATA_HOME")).split(":");
237   appDirs << QString(getenv("XDG_DATA_DIRS")).split(":");
238   if(appDirs.isEmpty()){ appDirs << "/usr/local/share" << "/usr/share" << LOS::AppPrefix()+"/share" << LOS::SysPrefix()+"/share" << L_SHAREDIR; }
239   appDirs.removeDuplicates();
240   //Now create a valid list
241   QStringList out;
242   for(int i=0; i<appDirs.length(); i++){
243     if( QFile::exists(appDirs[i]+"/applications") ){
244       out << appDirs[i]+"/applications";
245       //Also check any subdirs within this directory
246       // (looking at you KDE - stick to the standards!!)
247       out << LUtils::listSubDirectories(appDirs[i]+"/applications");
248     }
249   }
250   //qDebug() << "System Application Dirs:" << out;
251   return out;
252 }
253 
GenerateOpenTerminalExec(QString term,QString dirpath)254 QString LUtils::GenerateOpenTerminalExec(QString term, QString dirpath){
255   //Check the input terminal application (default/fallback - determined by calling application)
256   //if(!LUtils::isValidBinary(term)){
257     if(term.endsWith(".desktop")){
258       //Pull the binary name out of the shortcut
259       XDGDesktop DF(term);
260       if(DF.type == XDGDesktop::BAD){ term = "xterm"; }
261       else{ term= DF.exec.section(" ",0,0); } //only take the binary name - not any other flags
262     }else{
263 	term = "xterm"; //fallback
264     }
265   //}
266   //Now create the calling command for the designated terminal
267   // NOTE: While the "-e" routine is supposed to be universal, many terminals do not properly use it
268   //  so add some special/known terminals here as necessary
269   QString exec;
270   qWarning() << " - Reached terminal initialization" << term;
271   if(term=="mate-terminal" || term=="lxterminal" || term=="gnome-terminal"){
272     exec = term+" --working-directory=\""+dirpath+"\"";
273   }else if(term=="xfce4-terminal"){
274     exec = term+" --default-working-directory=\""+dirpath+"\"";
275   }else if(term=="konsole" || term == "qterminal"){
276     exec = term+" --workdir \""+dirpath+"\"";
277   }else{
278     //-e is the parameter for most of the terminal appliction to execute an external command.
279     //In this case we start a shell in the selected directory
280       //Need the user's shell first
281       QString shell = QString(getenv("SHELL"));
282       if(!LUtils::isValidBinary(shell)){ shell = "/bin/sh"; } //universal fallback for a shell
283     exec = term + " -e \"cd " + dirpath + " && " + shell + " \" ";
284   }
285   qDebug() << exec;
286   return exec;
287 }
288 
listSubDirectories(QString dir,bool recursive)289 QStringList LUtils::listSubDirectories(QString dir, bool recursive){
290   //This is a recursive method for returning the full paths of all subdirectories (if recursive flag is enabled)
291   QDir maindir(dir);
292   QStringList out;
293   QStringList subs = maindir.entryList(QDir::NoDotAndDotDot | QDir::Dirs, QDir::Name);
294   for(int i=0; i<subs.length(); i++){
295     out << maindir.absoluteFilePath(subs[i]);
296     if(recursive){
297       out << LUtils::listSubDirectories(maindir.absoluteFilePath(subs[i]), recursive);
298     }
299   }
300   return out;
301 }
302 
PathToAbsolute(QString path)303 QString LUtils::PathToAbsolute(QString path){
304   //Convert an input path to an absolute path (this does not check existance ot anything)
305   if(path.startsWith("/")){ return path; } //already an absolute path
306   if(path.startsWith("~")){ path.replace(0,1,QDir::homePath()); }
307   if(!path.startsWith("/")){
308     //Must be a relative path
309     if(path.startsWith("./")){ path = path.remove(2); }
310     path.prepend( QDir::currentPath()+"/");
311   }
312   return path;
313 }
314 
AppToAbsolute(QString path)315 QString LUtils::AppToAbsolute(QString path){
316   if(path.startsWith("~/")){ path = path.replace("~/", QDir::homePath()+"/" ); }
317   if(path.startsWith("/") || QFile::exists(path)){ return path; }
318   if(path.endsWith(".desktop")){
319     //Look in the XDG dirs
320     QStringList dirs = systemApplicationDirs();
321     for(int i=0; i<dirs.length(); i++){
322       if(QFile::exists(dirs[i]+"/"+path)){ return (dirs[i]+"/"+path); }
323     }
324   }else{
325     //Look on $PATH for the binary
326     QStringList paths = QString(getenv("PATH")).split(":");
327     for(int i=0; i<paths.length(); i++){
328       if(QFile::exists(paths[i]+"/"+path)){ return (paths[i]+"/"+path); }
329     }
330   }
331   return path;
332 }
333 
videoExtensions()334 QStringList LUtils::videoExtensions() {
335   static QStringList vidExtensions;
336   vidExtensions << "avi" << "mkv" << "mp4" << "mov" << "webm" << "wmv";
337   return vidExtensions;
338 }
339 
imageExtensions(bool wildcards)340 QStringList LUtils::imageExtensions(bool wildcards){
341   //Note that all the image extensions are lowercase!!
342   static QStringList imgExtensions;
343   static QStringList imgExtensionsWC;
344   if(imgExtensions.isEmpty()){
345     QList<QByteArray> fmt = QImageReader::supportedImageFormats();
346     for(int i=0; i<fmt.length(); i++){
347       imgExtensionsWC << "*."+QString::fromLocal8Bit(fmt[i]);
348       imgExtensions << QString::fromLocal8Bit(fmt[i]);
349     }
350   }
351   if(wildcards){ return imgExtensionsWC; }
352   return imgExtensions;
353 }
354 
LoadTranslation(QApplication * app,QString appname,QString locale,QTranslator * cTrans)355  QTranslator* LUtils::LoadTranslation(QApplication *app, QString appname, QString locale, QTranslator *cTrans){
356    //Get the current localization
357     QString langEnc = "UTF-8"; //default value
358     QString langCode = locale; //provided locale
359     if(langCode.isEmpty()){ langCode = getenv("LC_ALL"); }
360     if(langCode.isEmpty()){ langCode = getenv("LANG"); }
361     if(langCode.isEmpty()){ langCode = "en_US.UTF-8"; } //default to US english
362     //See if the encoding is included and strip it out as necessary
363     if(langCode.contains(".")){
364       langEnc = langCode.section(".",-1);
365       langCode = langCode.section(".",0,0);
366     }
367     //Now verify the encoding for the locale
368     if(langCode =="C" || langCode=="POSIX" || langCode.isEmpty()){
369       langEnc = "System"; //use the Qt system encoding
370     }
371     if(app !=0){
372       qDebug() << "Loading Locale:" << appname << langCode << langEnc;
373       //If an existing translator was provided, remove it first (will be replaced)
374       if(cTrans!=0){ app->removeTranslator(cTrans); }
375       //Setup the translator
376       cTrans = new QTranslator();
377       //Use the shortened locale code if specific code does not have a corresponding file
378       if(!QFile::exists(LOS::LuminaShare()+"i18n/"+appname+"_" + langCode + ".qm") && langCode!="en_US" ){
379         langCode.truncate( langCode.indexOf("_") );
380       }
381       QString filename = appname+"_"+langCode+".qm";
382       //qDebug() << "FileName:" << filename << "Dir:" << LOS::LuminaShare()+"i18n/";
383       if( cTrans->load( filename, LOS::LuminaShare()+"i18n/" ) ){
384         app->installTranslator( cTrans );
385       }else{
386 	//Translator could not be loaded for some reason
387 	cTrans = 0;
388 	if(langCode!="en_US"){
389 	  qWarning() << " - Could not load Locale:" << langCode;
390 	}
391       }
392     }else{
393       //Only going to set the encoding since no application given
394       qDebug() << "Loading System Encoding:" << langEnc;
395     }
396     //Load current encoding for this locale
397     QTextCodec::setCodecForLocale( QTextCodec::codecForName(langEnc.toUtf8()) );
398     return cTrans;
399 }
400 
knownLocales()401 QStringList LUtils::knownLocales(){
402   QDir i18n = QDir(LOS::LuminaShare()+"i18n");
403     if( !i18n.exists() ){ return QStringList(); }
404   QStringList files = i18n.entryList(QStringList() << "lumina-desktop_*.qm", QDir::Files, QDir::Name);
405     if(files.isEmpty()){ return QStringList(); }
406   //Now strip off the filename and just leave the locale tag
407   for(int i=0; i<files.length(); i++){
408      files[i].chop(3); //remove the ".qm" on the end
409      files[i] = files[i].section("_",1,50).simplified();
410   }
411   files << "en_US"; //default locale
412   files.sort();
413   return files;
414 }
415 
setLocaleEnv(QString lang,QString msg,QString time,QString num,QString money,QString collate,QString ctype)416 void LUtils::setLocaleEnv(QString lang, QString msg, QString time, QString num,QString money,QString collate, QString ctype){
417   //Adjust the current locale environment variables
418   bool all = false;
419   if(msg.isEmpty() && time.isEmpty() && num.isEmpty() && money.isEmpty() && collate.isEmpty() && ctype.isEmpty() ){
420     if(lang.isEmpty()){ return; } //nothing to do - no changes requested
421     all = true; //set everything to the "lang" value
422   }
423   //If no lang given, but others are given, then use the current setting
424   if(lang.isEmpty()){ lang = getenv("LC_ALL"); }
425   if(lang.isEmpty()){ lang = getenv("LANG"); }
426   if(lang.isEmpty()){ lang = "en_US"; }
427   //Now go through and set/unset the environment variables
428   // - LANG & LC_ALL
429   if(!lang.contains(".")){ lang.append(".UTF-8"); }
430   setenv("LANG",lang.toUtf8() ,1); //overwrite setting (this is always required as the fallback)
431   if(all){ setenv("LC_ALL",lang.toUtf8() ,1); }
432   else{ unsetenv("LC_ALL"); } //make sure the custom settings are used
433   // - LC_MESSAGES
434   if(msg.isEmpty()){ unsetenv("LC_MESSAGES"); }
435   else{
436     if(!msg.contains(".")){ msg.append(".UTF-8"); }
437     setenv("LC_MESSAGES",msg.toUtf8(),1);
438   }
439   // - LC_TIME
440   if(time.isEmpty()){ unsetenv("LC_TIME"); }
441   else{
442     if(!time.contains(".")){ time.append(".UTF-8"); }
443     setenv("LC_TIME",time.toUtf8(),1);
444   }
445     // - LC_NUMERIC
446   if(num.isEmpty()){ unsetenv("LC_NUMERIC"); }
447   else{
448     if(!num.contains(".")){ num.append(".UTF-8"); }
449     setenv("LC_NUMERIC",num.toUtf8(),1);
450   }
451     // - LC_MONETARY
452   if(money.isEmpty()){ unsetenv("LC_MONETARY"); }
453   else{
454     if(!money.contains(".")){ money.append(".UTF-8"); }
455     setenv("LC_MONETARY",money.toUtf8(),1);
456   }
457     // - LC_COLLATE
458   if(collate.isEmpty()){ unsetenv("LC_COLLATE"); }
459   else{
460     if(!collate.contains(".")){ collate.append(".UTF-8"); }
461     setenv("LC_COLLATE",collate.toUtf8(),1);
462   }
463     // - LC_CTYPE
464   if(ctype.isEmpty()){ unsetenv("LC_CTYPE"); }
465   else{
466     if(!ctype.contains(".")){ ctype.append(".UTF-8"); }
467     setenv("LC_CTYPE",ctype.toUtf8(),1);
468   }
469 }
470 
currentLocale()471 QString LUtils::currentLocale(){
472   QString curr = getenv("LC_ALL");// = QLocale::system();
473   if(curr.isEmpty()){ curr = getenv("LANG"); }
474   if(curr.isEmpty()){ curr = "en_US"; }
475   curr = curr.section(".",0,0); //remove any encodings off the end
476   return curr;
477 }
478 
DisplaySizeToBytes(QString num)479 double LUtils::DisplaySizeToBytes(QString num){
480   //qDebug() << "Convert Num to Bytes:" << num;
481   num = num.toLower().simplified();
482   num = num.remove(" ");
483   if(num.isEmpty()){ return 0.0; }
484   if(num.endsWith("b")){ num.chop(1); } //remove the "bytes" marker (if there is one)
485   QString lab = "b";
486   if(!num[num.size()-1].isNumber()){
487     lab = num.right(1); num.chop(1);
488   }
489   double N = num.toDouble();
490   QStringList labs; labs <<"b"<<"k"<<"m"<<"g"<<"t"<<"p"; //go up to petabytes for now
491   for(int i=0; i<labs.length(); i++){
492     if(lab==labs[i]){ break; }//already at the right units - break out
493     N = N*1024.0; //Move to the next unit of measurement
494   }
495   //qDebug() << " - Done:" << QString::number(N) << lab << num;
496   return N;
497 }
498 
BytesToDisplaySize(qint64 ibytes)499 QString LUtils::BytesToDisplaySize(qint64 ibytes){
500   static QStringList labs = QStringList();
501   if(labs.isEmpty()){ labs << "B" << "K" << "M" << "G" << "T" << "P"; }
502   //Now get the dominant unit
503   int c=0;
504   double bytes = ibytes; //need to keep decimel places for calculations
505   while(bytes>=1000 && c<labs.length() ){
506     bytes = bytes/1024;
507     c++;
508   } //labs[c] is the unit
509   //Bytes are now
510   //Now format the number (up to 3 digits, not including decimel places)
511   QString num;
512   if(bytes>=100){
513     //No decimel places
514     num = QString::number(qRound(bytes));
515   }else if(bytes>=10){
516     //need 1 decimel place
517     num = QString::number( (qRound(bytes*10)/10.0) );
518   }else if(bytes>=1){
519     //need 2 decimel places
520     num = QString::number( (qRound(bytes*100)/100.0) );
521   }else{
522     //Fully decimel (3 places)
523     num = "0."+QString::number(qRound(bytes*1000));
524   }
525   //qDebug() << "Bytes to Human-readable:" << bytes << c << num << labs[c];
526   return (num+labs[c]);
527 }
528 
SecondsToDisplay(int secs)529 QString LUtils::SecondsToDisplay(int secs){
530   if(secs < 0){ return "??"; }
531   QString rem; //remaining
532   if(secs > 3600){
533     int hours = secs/3600;
534     rem.append( QString::number(hours)+"h ");
535     secs = secs - (hours*3600);
536   }
537   if(secs > 60){
538     int min = secs/60;
539     rem.append( QString::number(min)+"m ");
540     secs = secs - (min*60);
541   }
542   if(secs > 0){
543     rem.append( QString::number(secs)+"s");
544   }else{
545     rem.append( "0s" );
546   }
547   return rem;
548 }
549