From be40411b667a79b613229d962b6ef100ea7019e9 Mon Sep 17 00:00:00 2001 From: Romain Failliot Date: Tue, 16 Nov 2021 22:42:29 -0500 Subject: [PATCH] Fix subprocess call from within Flatpak Fixes #103 --- CHANGELOG.md | 2 + data/diffuserc.in | 2 +- data/meson.build | 2 +- io.github.mightycreak.Diffuse.yml | 3 + meson_options.txt | 1 + src/constants.py.in | 27 +++++ src/diffuse.in | 4 +- src/main.py | 162 +++++++++--------------------- src/meson.build | 9 ++ src/utils.py | 74 ++++++++++++-- 10 files changed, 163 insertions(+), 123 deletions(-) create mode 100644 meson_options.txt create mode 100644 src/constants.py.in diff --git a/CHANGELOG.md b/CHANGELOG.md index c7b1b06..316d2b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed ### Fixed +- Fixed #103: the flatpak app can now call binaries on the host, such as `git`, + `svn`, etc. (PR #105) ## [0.7.0] - 2021-11-16 diff --git a/data/diffuserc.in b/data/diffuserc.in index 9a2f221..70b36dd 100644 --- a/data/diffuserc.in +++ b/data/diffuserc.in @@ -2,4 +2,4 @@ # # Copyright (C) 2006-2009 Derrick Moser -import @PKGDATADIR@/syntax/*.syntax +import @pkgdatadir@/syntax/*.syntax diff --git a/data/meson.build b/data/meson.build index 19a354a..10fae07 100644 --- a/data/meson.build +++ b/data/meson.build @@ -33,7 +33,7 @@ endif # Diffuse config file conf = configuration_data() -conf.set('PKGDATADIR', pkgdatadir) +conf.set('pkgdatadir', pkgdatadir) configure_file( input: 'diffuserc.in', diff --git a/io.github.mightycreak.Diffuse.yml b/io.github.mightycreak.Diffuse.yml index 288e50f..8bc70ba 100644 --- a/io.github.mightycreak.Diffuse.yml +++ b/io.github.mightycreak.Diffuse.yml @@ -8,10 +8,13 @@ finish-args: - --socket=fallback-x11 - --share=ipc - --filesystem=home + - --talk-name=org.freedesktop.Flatpak modules: - name: diffuse builddir: true buildsystem: meson + config-opts: + - -Duse_flatpak=true sources: - type: dir path: . diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..6857b44 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1 @@ +option('use_flatpak', type: 'boolean', value: false) diff --git a/src/constants.py.in b/src/constants.py.in new file mode 100644 index 0000000..d5d4e77 --- /dev/null +++ b/src/constants.py.in @@ -0,0 +1,27 @@ +# Diffuse: a graphical tool for merging and comparing text files. +# +# Copyright (C) 2019 Derrick Moser +# Copyright (C) 2021 Romain Failliot +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +APP_NAME = 'Diffuse' +VERSION = '@VERSION@' +COPYRIGHT = '''{copyright} © 2006-2019 Derrick Moser +{copyright} © 2015-2021 Romain Failliot'''.format(copyright=_("Copyright")) +WEBSITE = 'https://mightycreak.github.io/diffuse/' + +sysconfigdir = '@sysconfigdir@' +use_flatpak = @use_flatpak@ diff --git a/src/diffuse.in b/src/diffuse.in index 56da64c..dd38f22 100755 --- a/src/diffuse.in +++ b/src/diffuse.in @@ -23,10 +23,8 @@ import sys import gettext import locale -VERSION = '@VERSION@' pkgdatadir = '@pkgdatadir@' localedir = '@localedir@' -sysconfigdir = '@sysconfigdir@' sys.path.insert(1, pkgdatadir) gettext.install('diffuse', localedir) @@ -34,4 +32,4 @@ gettext.install('diffuse', localedir) if __name__ == '__main__': from diffuse import main - sys.exit(main.main(VERSION, sysconfigdir)) + sys.exit(main.main()) diff --git a/src/main.py b/src/main.py index 2c6b941..1b02e1c 100644 --- a/src/main.py +++ b/src/main.py @@ -26,7 +26,6 @@ import glob import re import shlex import stat -import subprocess import unicodedata import webbrowser @@ -53,6 +52,7 @@ from gi.repository import PangoCairo from urllib.parse import urlparse from diffuse import utils +from diffuse import constants if not hasattr(__builtins__, 'WindowsError'): # define 'WindowsError' so 'except' statements will work on all platforms @@ -739,7 +739,7 @@ class Preferences: [ 'List', [ 'Integer', 'tabs_default_panes', 2, _('Default panes'), 2, 16 ], [ 'Boolean', 'tabs_always_show', False, _('Always show the tab bar') ], - [ 'Boolean', 'tabs_warn_before_quit', True, _('Warn me when closing a tab will quit %s') % utils.APP_NAME ] + [ 'Boolean', 'tabs_warn_before_quit', True, _('Warn me when closing a tab will quit %s') % constants.APP_NAME ] ], _('Regional Settings'), [ 'List', @@ -900,7 +900,7 @@ class Preferences: ss.append(f'{k} "{v_escaped}"\n') ss.sort() f = open(self.path, 'w') - f.write(f'# This prefs file was generated by {utils.APP_NAME} {utils.VERSION}.\n\n') + f.write(f'# This prefs file was generated by {constants.APP_NAME} {constants.VERSION}.\n\n') for s in ss: f.write(s) f.close() @@ -1225,66 +1225,6 @@ def safeRelativePath(abspath1, name, prefs, cygwin_pref): def bashEscape(s): return "'" + s.replace("'", "'\\''") + "'" -# use popen to read the output of a command -def popenRead(dn, cmd, prefs, bash_pref, success_results=None): - if success_results is None: - success_results = [ 0 ] - if utils.isWindows() and prefs.getBool(bash_pref): - # launch the command from a bash shell is requested - cmd = [ prefs.convertToNativePath('/bin/bash.exe'), '-l', '-c', 'cd {}; {}'.format(bashEscape(dn), ' '.join([ bashEscape(arg) for arg in cmd ])) ] - dn = None - # use subprocess.Popen to retrieve the file contents - if utils.isWindows(): - info = subprocess.STARTUPINFO() - info.dwFlags |= subprocess.STARTF_USESHOWWINDOW - info.wShowWindow = subprocess.SW_HIDE - else: - info = None - proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dn, startupinfo=info) - proc.stdin.close() - proc.stderr.close() - fd = proc.stdout - # read the command's output - s = fd.read() - fd.close() - if proc.wait() not in success_results: - raise IOError('Command failed.') - return s - -# use popen to read the output of a command -def popenReadLines(dn, cmd, prefs, bash_pref, success_results=None): - return strip_eols(splitlines(popenRead(dn, cmd, prefs, bash_pref, success_results).decode('utf-8', errors='ignore'))) - -# simulate use of popen with xargs to read the output of a command -def popenXArgsReadLines(dn, cmd, args, prefs, bash_pref): - # os.sysconf() is only available on Unix - if hasattr(os, 'sysconf'): - maxsize = os.sysconf('SC_ARG_MAX') - maxsize -= sum([ len(k) + len(v) + 2 for k, v in os.environ.items() ]) - else: - # assume the Window's limit to CreateProcess() - maxsize = 32767 - maxsize -= sum([ len(k) + 1 for k in cmd ]) - - ss = [] - i, s, a = 0, 0, [] - while i < len(args): - f = (len(a) == 0) - if f: - # start a new command line - a = cmd[:] - elif s + len(args[i]) + 1 <= maxsize: - f = True - if f: - # append another argument to the current command line - a.append(args[i]) - s += len(args[i]) + 1 - i += 1 - if i == len(args) or not f: - ss.extend(popenReadLines(dn, a, prefs, bash_pref)) - s, a = 0, [] - return ss - # utility class to help support Git and Monotone # represents a set of files and folders of interest for "git status" or # "mtn automate inventory" @@ -1361,7 +1301,7 @@ class _Bzr: isabs |= os.path.isabs(name) args.append(safeRelativePath(self.root, name, prefs, 'bzr_cygwin')) # run command - ss = popenReadLines(self.root, args, prefs, 'bzr_bash') + ss = utils.popenReadLines(self.root, args, prefs, 'bzr_bash') # parse response prev = 'before:' + rev fs = _VcsFolderSet(names) @@ -1441,7 +1381,7 @@ class _Bzr: prev = '-1' fs = _VcsFolderSet(names) added, modified, removed, renamed = {}, {}, {}, {} - for s in popenReadLines(self.root, args, prefs, 'bzr_bash'): + for s in utils.popenReadLines(self.root, args, prefs, 'bzr_bash'): # parse response if len(s) < 5: continue @@ -1498,7 +1438,7 @@ class _Bzr: return result def getRevision(self, prefs, name, rev): - return popenRead(self.root, [ prefs.getString('bzr_bin'), 'cat', '--name-from-revision', '-r', rev, safeRelativePath(self.root, name, prefs, 'bzr_cygwin') ], prefs, 'bzr_bash') + return utils.popenRead(self.root, [ prefs.getString('bzr_bin'), 'cat', '--name-from-revision', '-r', rev, safeRelativePath(self.root, name, prefs, 'bzr_cygwin') ], prefs, 'bzr_bash') def _get_bzr_repo(path, prefs): p = _find_parent_dir_with(path, '.bzr') @@ -1547,7 +1487,7 @@ class _Cvs: prev = 'BASE' fs = _VcsFolderSet(names) modified = {} - for s in popenReadLines(self.root, args, prefs, 'cvs_bash'): + for s in utils.popenReadLines(self.root, args, prefs, 'cvs_bash'): # parse response if len(s) < 3 or s[0] not in 'ACMR': continue @@ -1571,10 +1511,10 @@ class _Cvs: def getRevision(self, prefs, name, rev): if rev == 'BASE' and not os.path.exists(name): # find revision for removed files - for s in popenReadLines(self.root, [ prefs.getString('cvs_bin'), 'status', safeRelativePath(self.root, name, prefs, 'cvs_cygwin') ], prefs, 'cvs_bash'): + for s in utils.popenReadLines(self.root, [ prefs.getString('cvs_bin'), 'status', safeRelativePath(self.root, name, prefs, 'cvs_cygwin') ], prefs, 'cvs_bash'): if s.startswith(' Working revision:\t-'): rev = s.split('\t')[1][1:] - return popenRead(self.root, [ prefs.getString('cvs_bin'), '-Q', 'update', '-p', '-r', rev, safeRelativePath(self.root, name, prefs, 'cvs_cygwin') ], prefs, 'cvs_bash') + return utils.popenRead(self.root, [ prefs.getString('cvs_bin'), '-Q', 'update', '-p', '-r', rev, safeRelativePath(self.root, name, prefs, 'cvs_cygwin') ], prefs, 'cvs_bash') def _get_cvs_repo(path, prefs): if os.path.isdir(os.path.join(path, 'CVS')): @@ -1608,7 +1548,7 @@ class _Darcs: args.append(safeRelativePath(self.root, name, prefs, 'darcs_cygwin')) # run command # 'darcs whatsnew' will return 1 if there are no changes - ss = popenReadLines(self.root, args, prefs, 'darcs_bash', [0, 1]) + ss = utils.popenReadLines(self.root, args, prefs, 'darcs_bash', [0, 1]) # parse response i, n = 0, len(ss) if mods: @@ -1701,7 +1641,7 @@ class _Darcs: except ValueError: args.extend([ '-h', rev ]) args.append(safeRelativePath(self.root, name, prefs, 'darcs_cygwin')) - return popenRead(self.root, args, prefs, 'darcs_bash') + return utils.popenRead(self.root, args, prefs, 'darcs_bash') def _get_darcs_repo(path, prefs): p = _find_parent_dir_with(path, '_darcs') @@ -1727,7 +1667,7 @@ class _Git: prev = rev + '^' fs = _VcsFolderSet(names) modified = {} - for s in popenReadLines(self.root, args, prefs, 'git_bash'): + for s in utils.popenReadLines(self.root, args, prefs, 'git_bash'): # parse response if len(s) < 2 or s[0] not in 'ADM': continue @@ -1762,7 +1702,7 @@ class _Git: fs = _VcsFolderSet(names) modified, renamed = {}, {} # 'git status' will return 1 when a commit would fail - for s in popenReadLines(self.root, args, prefs, 'git_bash', [0, 1]): + for s in utils.popenReadLines(self.root, args, prefs, 'git_bash', [0, 1]): # parse response if len(s) < 3: continue @@ -1828,13 +1768,13 @@ class _Git: return result def getRevision(self, prefs, name, rev): - return popenRead(self.root, [ prefs.getString('git_bin'), 'show', '{}:{}'.format(rev, relpath(self.root, os.path.abspath(name)).replace(os.sep, '/')) ], prefs, 'git_bash') + return utils.popenRead(self.root, [ prefs.getString('git_bin'), 'show', '{}:{}'.format(rev, relpath(self.root, os.path.abspath(name)).replace(os.sep, '/')) ], prefs, 'git_bash') def _get_git_repo(path, prefs): if 'GIT_DIR' in os.environ: try: d = path - ss = popenReadLines(d, [ prefs.getString('git_bin'), 'rev-parse', '--show-prefix' ], prefs, 'git_bash') + ss = utils.popenReadLines(d, [ prefs.getString('git_bin'), 'rev-parse', '--show-prefix' ], prefs, 'git_bash') if len(ss) > 0: # be careful to handle trailing slashes d = d.split(os.sep) @@ -1873,7 +1813,7 @@ class _Hg: def _getPreviousRevision(self, prefs, rev): if rev is None: if self.working_rev is None: - ss = popenReadLines(self.root, [ prefs.getString('hg_bin'), 'id', '-i', '-t' ], prefs, 'hg_bash') + ss = utils.popenReadLines(self.root, [ prefs.getString('hg_bin'), 'id', '-i', '-t' ], prefs, 'hg_bash') if len(ss) != 1: raise IOError('Unknown working revision') ss = ss[0].split(' ') @@ -1901,7 +1841,7 @@ class _Hg: prev = self._getPreviousRevision(prefs, rev) fs = _VcsFolderSet(names) modified = {} - for s in popenReadLines(self.root, args, prefs, 'hg_bash'): + for s in utils.popenReadLines(self.root, args, prefs, 'hg_bash'): # parse response if len(s) < 3 or s[0] not in 'AMR': continue @@ -1928,7 +1868,7 @@ class _Hg: return self._getCommitTemplate(prefs, names, [ 'status', '-q' ], None) def getRevision(self, prefs, name, rev): - return popenRead(self.root, [ prefs.getString('hg_bin'), 'cat', '-r', rev, safeRelativePath(self.root, name, prefs, 'hg_cygwin') ], prefs, 'hg_bash') + return utils.popenRead(self.root, [ prefs.getString('hg_bin'), 'cat', '-r', rev, safeRelativePath(self.root, name, prefs, 'hg_cygwin') ], prefs, 'hg_bash') def _get_hg_repo(path, prefs): p = _find_parent_dir_with(path, '.hg') @@ -1947,7 +1887,7 @@ class _Mtn: def getCommitTemplate(self, prefs, rev, names): # build command vcs_bin = prefs.getString('mtn_bin') - ss = popenReadLines(self.root, [ vcs_bin, 'automate', 'select', '-q', rev ], prefs, 'mtn_bash') + ss = utils.popenReadLines(self.root, [ vcs_bin, 'automate', 'select', '-q', rev ], prefs, 'mtn_bash') if len(ss) != 1: raise IOError('Ambiguous revision specifier') args = [ vcs_bin, 'automate', 'get_revision', ss[0] ] @@ -1959,7 +1899,7 @@ class _Mtn: # run command prev = None removed, added, modified, renamed = {}, {}, {}, {} - ss = popenReadLines(self.root, args, prefs, 'mtn_bash') + ss = utils.popenReadLines(self.root, args, prefs, 'mtn_bash') i = 0 while i < len(ss): # process results @@ -2002,7 +1942,7 @@ class _Mtn: if removed or renamed: # remove directories removed_dirs = set() - for s in popenReadLines(self.root, [ vcs_bin, 'automate', 'get_manifest_of', prev ], prefs, 'mtn_bash'): + for s in utils.popenReadLines(self.root, [ vcs_bin, 'automate', 'get_manifest_of', prev ], prefs, 'mtn_bash'): s = shlex.split(s) if len(s) > 1 and s[0] == 'dir': removed_dirs.add(s[1]) @@ -2049,7 +1989,7 @@ class _Mtn: isabs |= os.path.isabs(name) # build list of interesting files prev = 'h:' - ss = popenReadLines(self.root, args, prefs, 'mtn_bash') + ss = utils.popenReadLines(self.root, args, prefs, 'mtn_bash') removed, added, modified, renamed = {}, {}, {}, {} i = 0 while i < len(ss): @@ -2110,7 +2050,7 @@ class _Mtn: return result def getRevision(self, prefs, name, rev): - return popenRead(self.root, [ prefs.getString('mtn_bin'), 'automate', 'get_file_of', '-q', '-r', rev, safeRelativePath(self.root, name, prefs, 'mtn_cygwin') ], prefs, 'mtn_bash') + return utils.popenRead(self.root, [ prefs.getString('mtn_bin'), 'automate', 'get_file_of', '-q', '-r', rev, safeRelativePath(self.root, name, prefs, 'mtn_cygwin') ], prefs, 'mtn_bash') def _get_mtn_repo(path, prefs): p = _find_parent_dir_with(path, '_MTN') @@ -2125,7 +2065,7 @@ class _Rcs: def getFileTemplate(self, prefs, name): args = [ prefs.getString('rcs_bin_rlog'), '-L', '-h', safeRelativePath(self.root, name, prefs, 'rcs_cygwin') ] rev = '' - for line in popenReadLines(self.root, args, prefs, 'rcs_bash'): + for line in utils.popenReadLines(self.root, args, prefs, 'rcs_bash'): if line.startswith('head: '): rev = line[6:] return [ (name, rev), (name, None) ] @@ -2201,7 +2141,7 @@ class _Rcs: args = [ safeRelativePath(self.root, k, prefs, 'rcs_cygwin') for k in r ] # run command r, k = {}, '' - for line in popenXArgsReadLines(self.root, cmd, args, prefs, 'rcs_bash'): + for line in utils.popenXArgsReadLines(self.root, cmd, args, prefs, 'rcs_bash'): # parse response if line.startswith('Working file: '): k = prefs.convertToNativePath(line[14:]) @@ -2214,7 +2154,7 @@ class _Rcs: return [ [ (k, r[k]), (k, None) ] for k in sorted(r.keys()) ] def getRevision(self, prefs, name, rev): - return popenRead(self.root, [ prefs.getString('rcs_bin_co'), '-p', '-q', '-r' + rev, safeRelativePath(self.root, name, prefs, 'rcs_cygwin') ], prefs, 'rcs_bash') + return utils.popenRead(self.root, [ prefs.getString('rcs_bin_co'), '-p', '-q', '-r' + rev, safeRelativePath(self.root, name, prefs, 'rcs_cygwin') ], prefs, 'rcs_bash') def _get_rcs_repo(path, prefs): if os.path.isdir(os.path.join(path, 'RCS')): @@ -2265,7 +2205,7 @@ class _Svn: vcs, prefix = self._getVcs(), self._getURLPrefix() n = len(prefix) args = [ prefs.getString(vcs + '_bin'), 'info' ] - for s in popenReadLines(self.root, args, prefs, vcs + '_bash'): + for s in utils.popenReadLines(self.root, args, prefs, vcs + '_bash'): if s.startswith(prefix): self.url = s[n:] break @@ -2312,7 +2252,7 @@ class _Svn: # run command fs = _VcsFolderSet(names) modified, added, removed = {}, set(), set() - for s in popenReadLines(self.root, args, prefs, vcs_bash): + for s in utils.popenReadLines(self.root, args, prefs, vcs_bash): status = self._parseStatusLine(s) if status is None: continue @@ -2368,7 +2308,7 @@ class _Svn: # determine which are directories added = {} for p, v in m.items(): - for s in popenReadLines(self.root, [ vcs_bin, 'list', '-r', rev, '{}/{}'.format(self._getURL(prefs), p.replace(os.sep, '/')) ], prefs, vcs_bash): + for s in utils.popenReadLines(self.root, [ vcs_bin, 'list', '-r', rev, '{}/{}'.format(self._getURL(prefs), p.replace(os.sep, '/')) ], prefs, vcs_bash): if s in v: # confirmed as added file k = os.path.join(self.root, os.path.join(p, s)) @@ -2394,7 +2334,7 @@ class _Svn: m[d].add(b) removed_dir, removed = set(), {} for p, v in m.items(): - for s in popenReadLines(self.root, [ vcs_bin, 'list', '-r', prev, '{}/{}'.format(self._getURL(prefs), p.replace(os.sep, '/')) ], prefs, vcs_bash): + for s in utils.popenReadLines(self.root, [ vcs_bin, 'list', '-r', prev, '{}/{}'.format(self._getURL(prefs), p.replace(os.sep, '/')) ], prefs, vcs_bash): if s.endswith('/'): s = s[:-1] if s in v: @@ -2412,7 +2352,7 @@ class _Svn: tmp = removed_dir removed_dir = set() for p in tmp: - for s in popenReadLines(self.root, [ vcs_bin, 'list', '-r', prev, '{}/{}'.format(self._getURL(prefs), p.replace(os.sep, '/')) ], prefs, vcs_bash): + for s in utils.popenReadLines(self.root, [ vcs_bin, 'list', '-r', prev, '{}/{}'.format(self._getURL(prefs), p.replace(os.sep, '/')) ], prefs, vcs_bash): if s.endswith('/'): # confirmed item as directory removed_dir.add(os.path.join(p, s[:-1])) @@ -2441,8 +2381,8 @@ class _Svn: def getRevision(self, prefs, name, rev): vcs_bin = prefs.getString('svn_bin') if rev in [ 'BASE', 'COMMITTED', 'PREV' ]: - return popenRead(self.root, [ vcs_bin, 'cat', '{}@{}'.format(safeRelativePath(self.root, name, prefs, 'svn_cygwin'), rev) ], prefs, 'svn_bash') - return popenRead(self.root, [ vcs_bin, 'cat', '{}/{}@{}'.format(self._getURL(prefs), relpath(self.root, os.path.abspath(name)).replace(os.sep, '/'), rev) ], prefs, 'svn_bash') + return utils.popenRead(self.root, [ vcs_bin, 'cat', '{}@{}'.format(safeRelativePath(self.root, name, prefs, 'svn_cygwin'), rev) ], prefs, 'svn_bash') + return utils.popenRead(self.root, [ vcs_bin, 'cat', '{}/{}@{}'.format(self._getURL(prefs), relpath(self.root, os.path.abspath(name)).replace(os.sep, '/'), rev) ], prefs, 'svn_bash') def _get_svn_repo(path, prefs): p = _find_parent_dir_with(path, '.svn') @@ -2472,7 +2412,7 @@ class _Svk(_Svn): return str(int(rev) - 1) def getRevision(self, prefs, name, rev): - return popenRead(self.root, [ prefs.getString('svk_bin'), 'cat', '-r', rev, '{}/{}'.format(self._getURL(prefs), relpath(self.root, os.path.abspath(name)).replace(os.sep, '/')) ], prefs, 'svk_bash') + return utils.popenRead(self.root, [ prefs.getString('svk_bin'), 'cat', '-r', rev, '{}/{}'.format(self._getURL(prefs), relpath(self.root, os.path.abspath(name)).replace(os.sep, '/')) ], prefs, 'svk_bash') def _get_svk_repo(path, prefs): name = path @@ -6548,7 +6488,7 @@ class SearchDialog(Gtk.Dialog): # convenience method to request confirmation when closing the last tab def confirmTabClose(parent): - dialog = utils.MessageDialog(parent, Gtk.MessageType.WARNING, _('Closing this tab will quit %s.') % (utils.APP_NAME, )) + dialog = utils.MessageDialog(parent, Gtk.MessageType.WARNING, _('Closing this tab will quit %s.') % (constants.APP_NAME, )) end = (dialog.run() == Gtk.ResponseType.OK) dialog.destroy() return end @@ -6642,17 +6582,17 @@ class AboutDialog(Gtk.AboutDialog): def __init__(self): Gtk.AboutDialog.__init__(self) self.set_logo_icon_name('io.github.mightycreak.Diffuse') - self.set_program_name(utils.APP_NAME) - self.set_version(utils.VERSION) + self.set_program_name(constants.APP_NAME) + self.set_version(constants.VERSION) self.set_comments(_('Diffuse is a graphical tool for merging and comparing text files.')) - self.set_copyright(utils.COPYRIGHT) - self.set_website(utils.WEBSITE) + self.set_copyright(constants.COPYRIGHT) + self.set_website(constants.WEBSITE) self.set_authors([ 'Derrick Moser ', 'Romain Failliot ' ]) self.set_translator_credits(_('translator-credits')) license_text = [ - utils.APP_NAME + ' ' + utils.VERSION + '\n\n', - utils.COPYRIGHT + '\n\n', + constants.APP_NAME + ' ' + constants.VERSION + '\n\n', + constants.COPYRIGHT + '\n\n', _('''This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or @@ -6898,7 +6838,7 @@ class Diffuse(Gtk.Window): if self.headers[f].has_edits: # warn users of any unsaved changes they might lose dialog = Gtk.MessageDialog(self.get_toplevel(), Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.WARNING, Gtk.ButtonsType.NONE, _('Save changes before loading the new file?')) - dialog.set_title(utils.APP_NAME) + dialog.set_title(constants.APP_NAME) dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) dialog.add_button(Gtk.STOCK_NO, Gtk.ResponseType.REJECT) dialog.add_button(Gtk.STOCK_YES, Gtk.ResponseType.OK) @@ -7444,7 +7384,7 @@ class Diffuse(Gtk.Window): menuspecs.append([ _('_Help'), [ [_('_Help Contents...'), self.help_contents_cb, None, Gtk.STOCK_HELP, 'help_contents'], [], - [_('_About %s...') % (utils.APP_NAME, ), self.about_cb, None, Gtk.STOCK_ABOUT, 'about'] ] ]) + [_('_About %s...') % (constants.APP_NAME, ), self.about_cb, None, Gtk.STOCK_ABOUT, 'about'] ] ]) # used to disable menu events when switching tabs self.menu_update_depth = 0 @@ -7561,7 +7501,7 @@ class Diffuse(Gtk.Window): ss.append(f'{k} {v}\n') ss.sort() f = open(statepath, 'w') - f.write(f"# This state file was generated by {utils.APP_NAME} {utils.VERSION}.\n\n") + f.write(f"# This state file was generated by {constants.APP_NAME} {constants.VERSION}.\n\n") for s in ss: f.write(s) f.close() @@ -7601,7 +7541,7 @@ class Diffuse(Gtk.Window): buttons=Gtk.ButtonsType.NONE, text=_('Some files have unsaved changes. Select the files to save before closing.')) dialog.set_resizable(True) - dialog.set_title(utils.APP_NAME) + dialog.set_title(constants.APP_NAME) # add list of files with unsaved changes sw = Gtk.ScrolledWindow.new() sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) @@ -7688,7 +7628,7 @@ class Diffuse(Gtk.Window): # update window's title def updateTitle(self, viewer): title = self.notebook.get_tab_label(viewer).get_text() - self.set_title(f'{title} - {utils.APP_NAME}') + self.set_title(f'{title} - {constants.APP_NAME}') # update the message in the status bar def setStatus(self, s): @@ -8172,7 +8112,7 @@ class Diffuse(Gtk.Window): del parts[-1] if help_url is None: # no local help file is available, show on-line help - help_url = utils.WEBSITE + 'manual.html' + help_url = constants.WEBSITE + 'manual.html' # ask for localised manual if utils.lang is not None: help_url += '?lang=' + utils.lang @@ -8193,17 +8133,15 @@ GObject.signal_new('reload', Diffuse.FileDiffViewer.PaneHeader, GObject.SignalFl GObject.signal_new('save', Diffuse.FileDiffViewer.PaneHeader, GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ()) GObject.signal_new('save-as', Diffuse.FileDiffViewer.PaneHeader, GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ()) -def main(version, sysconfigdir): +def main(): # app = Application() # return app.run(sys.argv) - utils.VERSION = version - args = sys.argv argc = len(args) if argc == 2 and args[1] in [ '-v', '--version' ]: - print('%s %s\n%s' % (utils.APP_NAME, utils.VERSION, utils.COPYRIGHT)) + print('%s %s\n%s' % (constants.APP_NAME, constants.VERSION, constants.COPYRIGHT)) sys.exit(0) if argc == 2 and args[1] in [ '-h', '-?', '--help' ]: print(_('''Usage: @@ -8270,7 +8208,7 @@ Display Options: if utils.isWindows(): rc_file = os.path.join(utils.bin_dir, 'diffuserc') else: - rc_file = os.path.join(utils.bin_dir, f'{sysconfigdir}/diffuserc') + rc_file = os.path.join(utils.bin_dir, f'{constants.sysconfigdir}/diffuserc') for rc_file in rc_file, os.path.join(rc_dir, 'diffuserc'): if os.path.isfile(rc_file): rc_files.append(rc_file) diff --git a/src/meson.build b/src/meson.build index b93609c..f5e5ad7 100644 --- a/src/meson.build +++ b/src/meson.build @@ -10,6 +10,7 @@ conf.set('VERSION', meson.project_version()) conf.set('localedir', join_paths(get_option('prefix'), get_option('localedir'))) conf.set('pkgdatadir', pkgdatadir) conf.set('sysconfigdir', sysconfdir) +conf.set('use_flatpak', get_option('use_flatpak')) configure_file( input: 'diffuse.in', @@ -19,6 +20,14 @@ configure_file( install_dir: get_option('bindir') ) +configure_file( + input: 'constants.py.in', + output: 'constants.py', + configuration: conf, + install: true, + install_dir: moduledir +) + diffuse_sources = [ '__init__.py', 'main.py', diff --git a/src/utils.py b/src/utils.py index a6ffca9..02f905e 100644 --- a/src/utils.py +++ b/src/utils.py @@ -20,12 +20,15 @@ import os import sys import locale +import subprocess import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk +from diffuse import constants + # convenience class for displaying a message dialogue class MessageDialog(Gtk.MessageDialog): def __init__(self, parent, type, s): @@ -61,6 +64,71 @@ def make_subdirs(p, ss): pass return p +def useFlatpak(): + return constants.use_flatpak + +# use popen to read the output of a command +def popenRead(dn, cmd, prefs, bash_pref, success_results=None): + if success_results is None: + success_results = [ 0 ] + if isWindows() and prefs.getBool(bash_pref): + # launch the command from a bash shell is requested + cmd = [ prefs.convertToNativePath('/bin/bash.exe'), '-l', '-c', 'cd {}; {}'.format(bashEscape(dn), ' '.join([ bashEscape(arg) for arg in cmd ])) ] + dn = None + # use subprocess.Popen to retrieve the file contents + if isWindows(): + info = subprocess.STARTUPINFO() + info.dwFlags |= subprocess.STARTF_USESHOWWINDOW + info.wShowWindow = subprocess.SW_HIDE + else: + info = None + if useFlatpak(): + cmd = [ 'flatpak-spawn', '--host' ] + cmd + proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=dn, startupinfo=info) + proc.stdin.close() + proc.stderr.close() + fd = proc.stdout + # read the command's output + s = fd.read() + fd.close() + if proc.wait() not in success_results: + raise IOError('Command failed.') + return s + +# use popen to read the output of a command +def popenReadLines(dn, cmd, prefs, bash_pref, success_results=None): + return strip_eols(splitlines(popenRead(dn, cmd, prefs, bash_pref, success_results).decode('utf-8', errors='ignore'))) + +# simulate use of popen with xargs to read the output of a command +def popenXArgsReadLines(dn, cmd, args, prefs, bash_pref): + # os.sysconf() is only available on Unix + if hasattr(os, 'sysconf'): + maxsize = os.sysconf('SC_ARG_MAX') + maxsize -= sum([ len(k) + len(v) + 2 for k, v in os.environ.items() ]) + else: + # assume the Window's limit to CreateProcess() + maxsize = 32767 + maxsize -= sum([ len(k) + 1 for k in cmd ]) + + ss = [] + i, s, a = 0, 0, [] + while i < len(args): + f = (len(a) == 0) + if f: + # start a new command line + a = cmd[:] + elif s + len(args[i]) + 1 <= maxsize: + f = True + if f: + # append another argument to the current command line + a.append(args[i]) + s += len(args[i]) + 1 + i += 1 + if i == len(args) or not f: + ss.extend(popenReadLines(dn, a, prefs, bash_pref)) + s, a = 0, [] + return ss + # use the program's location as a starting place to search for supporting files # such as icon and help documentation if hasattr(sys, 'frozen'): @@ -85,9 +153,3 @@ if isWindows(): else: if lang is not None: os.environ['LANG'] = lang - -APP_NAME = 'Diffuse' -VERSION = '0.0.0' -COPYRIGHT = '''{copyright} © 2006-2019 Derrick Moser -{copyright} © 2015-2021 Romain Failliot'''.format(copyright=_("Copyright")) -WEBSITE = 'https://mightycreak.github.io/diffuse/'