Merge pull request #31 from MightyCreak/split-code-into-modules

Start to modularize code
This commit is contained in:
Creak 2021-11-16 17:51:10 -05:00 committed by GitHub
commit ea231a3444
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 737 additions and 596 deletions

View File

@ -26,12 +26,15 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-python@v2 - uses: actions/setup-python@v2
- name: Meson Build - name: Install dependencies
uses: BSFishy/meson-build@v1.0.1 run: sudo apt-get -y install appstream appstream-util desktop-file-utils gettext
- name: Meson build
uses: BSFishy/meson-build@v1.0.3
with: with:
action: build action: build
- name: Meson Test - name: Meson test
uses: BSFishy/meson-build@v1.0.1 uses: BSFishy/meson-build@v1.0.3
with: with:
action: test action: test

3
.gitignore vendored
View File

@ -13,8 +13,7 @@ __pycache__/
# Distribution / packaging # Distribution / packaging
.Python .Python
build/ build/
builddir/ build-flatpak/
builddir-flatpak/
develop-eggs/ develop-eggs/
dist/ dist/
downloads/ downloads/

28
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,28 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Debug",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/main.py",
"console": "integratedTerminal",
// "env": {
// "PYTHONPATH": "."
// }
},
{
"name": "Python: Remote Attach",
"type": "python",
"request": "attach",
"port": 5678,
"host": "localhost",
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "."
}
]
}
]
}

View File

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added MetaInfo file - Added MetaInfo file
- New SVG icon (thanks @creepertron95, @jimmac and @freddii) - New SVG icon (thanks @creepertron95, @jimmac and @freddii)
- Started modularizing the code
### Changed ### Changed
- Changed AppID to io.github.mightycreak.Diffuse (as explained in - Changed AppID to io.github.mightycreak.Diffuse (as explained in
@ -21,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add .desktop translations in .po files - Add .desktop translations in .po files
### Fixed ### Fixed
- Fixed some GTK deprecation warnings
## [0.6.0] - 2020-11-29 ## [0.6.0] - 2020-11-29

View File

@ -20,48 +20,71 @@ Some key features of Diffuse:
## Requirements ## Requirements
Diffuse is implemented entirely in Python and should run on any platform with
Python and PyGObject.
* Python >= 3.4 * Python >= 3.4
* PyGObject >= 3.18 * PyGObject >= 3.18
Diffuse is implemented entirely in Python and should run on any platform with ## Users
Python and PyGTK. If you need to manually install PyGTK, please be aware its
dependencies should be installed prior to installing PyGTK.
Diffuse can be run directly from an untared source distribution on any POSIX ### Installing using Flatpak
system or installed with the instructions described in the next section.
The location of the personal preferences, state, and initialisation files have This is the easiest way to install Diffuse:
changed in the 0.4.1 release. Old settings may be migrated using the following
commands:
$ mkdir -p ~/.config/diffuse ```sh
$ mv ~/.diffuse/config ~/.config/diffuse/state flatpak install io.github.mightycreak.Diffuse
$ mv ~/.diffuse/* ~/.config/diffuse ```
$ rmdir ~/.diffuse
The rules for parsing files in `~/.diffuse` changed in the 0.3.0 release. ## Developers
Non-fatal errors may be reported when parsing old files. These errors can be
fixed by removing the offending lines (or the entire file) from
`~/.config/diffuse/diffuserc`.
## Installing on POSIX systems ### Setup
#### Run Diffuse from source
To run Diffuse from the source code, type this:
```sh
python main.py
```
To debug with VS Code, open the directory in VS Code, place your breakpoints and hit F5.
#### Build Diffuse
To build Diffuse, type this:
```sh
python setup.py build
```
To run from the build, type this:
```sh
PYTHONPATH=build/lib ./build/scripts-3.7/diffuse
```
#### Install Diffuse locally
Diffuse build system is meson. Diffuse build system is meson.
To install diffuse locally: To install diffuse locally:
meson builddir ```sh
meson install -C builddir meson setup build
cd build
meson compile
meson install # requires admin privileges
```
To uninstall diffuse afterwards: To uninstall diffuse afterwards:
sudo ninja uninstall -C builddir ```sh
sudo rm -v /usr/local/share/locale/*/LC_MESSAGES/diffuse.mo sudo ninja uninstall -C build
sudo rm -v /usr/local/share/locale/*/LC_MESSAGES/diffuse.mo
```
Meson allows to change the default installation directories, see Meson allows to change the default installation directories, see
[command-line documentation](https://mesonbuild.com/Commands.html#configure). [command-line documentation](https://mesonbuild.com/Commands.html#configure).
## Installing on Windows ### Installing on Windows
The `windows-installer` directory contains scripts for building an installable The `windows-installer` directory contains scripts for building an installable
package for Windows that includes all dependencies. package for Windows that includes all dependencies.
@ -73,24 +96,26 @@ Diffuse. The `XDG_CONFIG_HOME` and `XDG_DATA_DIR` environment variables
indicate where Diffuse should store persistent settings (eg. the path to a indicate where Diffuse should store persistent settings (eg. the path to a
writable directory on the pen drive). writable directory on the pen drive).
## Installing the Flatpak package
flatpak install io.github.mightycreak.Diffuse
## Building and testing the Flatpak package ## Building and testing the Flatpak package
To install Diffuse locally: To install Diffuse locally:
flatpak install flatpak install runtime/org.gnome.Sdk/$(uname -p)/3.38 ```sh
flatpak-builder builddir-flatpak --user --install io.github.mightycreak.Diffuse.yml flatpak install runtime/org.gnome.Sdk/$(uname -p)/3.38
flatpak-builder build-flatpak --user --install io.github.mightycreak.Diffuse.yml
```
To run Diffuse through Flatpak: To run Diffuse through Flatpak:
flatpak run io.github.mightycreak.Diffuse ```sh
flatpak run io.github.mightycreak.Diffuse
```
To uninstall Diffuse: To uninstall Diffuse:
flatpak remove io.github.mightycreak.Diffuse ```sh
flatpak remove io.github.mightycreak.Diffuse
```
## Help Documentation ## Help Documentation

View File

@ -1,7 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sysconfig
from compileall import compile_dir
from os import environ, path from os import environ, path
from subprocess import call from subprocess import call
@ -13,3 +11,9 @@ destdir = environ.get('DESTDIR', '')
if not destdir: if not destdir:
print('Updating icon cache...') print('Updating icon cache...')
call(['gtk-update-icon-cache', '-qtf', path.join(datadir, 'icons', 'hicolor')]) call(['gtk-update-icon-cache', '-qtf', path.join(datadir, 'icons', 'hicolor')])
print('Updating desktop database...')
call(['update-desktop-database', '-q', path.join(datadir, 'applications')])
print('Compiling GSettings schemas...')
call(['glib-compile-schemas', path.join(datadir, 'glib-2.0', 'schemas')])

13
data/icons/meson.build Normal file
View File

@ -0,0 +1,13 @@
application_id = 'io.github.mightycreak.Diffuse'
scalable_dir = join_paths('hicolor', 'scalable', 'apps')
install_data(
join_paths(scalable_dir, ('@0@.svg').format(application_id)),
install_dir: join_paths(get_option('datadir'), 'icons', scalable_dir)
)
symbolic_dir = join_paths('hicolor', 'symbolic', 'apps')
install_data(
join_paths(symbolic_dir, ('@0@-symbolic.svg').format(application_id)),
install_dir: join_paths(get_option('datadir'), 'icons', symbolic_dir)
)

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application"> <component type="desktop-application">
<id>io.github.mightycreak.Diffuse</id> <id>io.github.mightycreak.Diffuse</id>
<name>Diffuse Merge Tool</name> <name>Diffuse Merge Tool</name>
<summary>Graphical tool for merging and comparing text files</summary> <summary>Graphical tool for merging and comparing text files</summary>
<description> <description>
@ -9,23 +8,18 @@
Diffuse is a graphical tool for comparing and merging text files. It can retrieve files for comparison from Bazaar, CVS, Darcs, Git, Mercurial, Monotone, RCS, Subversion, and SVK repositories. Diffuse is a graphical tool for comparing and merging text files. It can retrieve files for comparison from Bazaar, CVS, Darcs, Git, Mercurial, Monotone, RCS, Subversion, and SVK repositories.
</p> </p>
</description> </description>
<metadata_license>FSFAP</metadata_license> <metadata_license>FSFAP</metadata_license>
<project_license>GPL-2.0-or-later</project_license> <project_license>GPL-2.0-or-later</project_license>
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1"/>
<launchable type="desktop-id">io.github.mightycreak.Diffuse.desktop</launchable> <launchable type="desktop-id">io.github.mightycreak.Diffuse.desktop</launchable>
<url type="homepage">https://mightycreak.github.io/diffuse/</url> <url type="homepage">https://mightycreak.github.io/diffuse/</url>
<url type="bugtracker">https://github.com/MightyCreak/diffuse/issues</url> <url type="bugtracker">https://github.com/MightyCreak/diffuse/issues</url>
<screenshots> <screenshots>
<screenshot type="default"> <screenshot type="default">
<image>https://mightycreak.github.io/diffuse/images/screenshot_v0.5.0.png</image>
<caption>Main window: diff between two files</caption> <caption>Main window: diff between two files</caption>
<image>https://mightycreak.github.io/diffuse/images/screenshot_v0.7.0.png</image>
</screenshot> </screenshot>
</screenshots> </screenshots>
<releases> <releases>
<release version="0.6.0" date="2020-11-29"> <release version="0.6.0" date="2020-11-29">
<description> <description>
@ -102,7 +96,6 @@
</description> </description>
</release> </release>
</releases> </releases>
<developer_name>Romain Failliot</developer_name> <developer_name>Romain Failliot</developer_name>
<update_contact>romain.failliot@foolstep.com</update_contact> <update_contact>romain.failliot@foolstep.com</update_contact>
</component> </component>

View File

@ -1,10 +1,49 @@
desktop_file = 'diffuse.desktop' pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name())
i18n.merge_file(
desktop_file, desktop_file = i18n.merge_file(
input: desktop_file + '.in', input: 'io.github.mightycreak.Diffuse.desktop.in',
output: desktop_file, output: 'io.github.mightycreak.Diffuse.desktop',
type: 'desktop',
po_dir: '../po', po_dir: '../po',
install: true, install: true,
install_dir: join_paths(datadir, 'applications'), install_dir: join_paths(get_option('datadir'), 'applications')
type: 'desktop'
) )
desktop_utils = find_program('desktop-file-validate', required: false)
if desktop_utils.found()
test('Validate desktop file', desktop_utils,
args: [desktop_file]
)
endif
appstream_file = i18n.merge_file(
input: 'io.github.mightycreak.Diffuse.metainfo.xml.in',
output: 'io.github.mightycreak.Diffuse.metainfo.xml',
po_dir: '../po',
install: true,
install_dir: join_paths(get_option('datadir'), 'appdata')
)
appstream_util = find_program('appstream-util', required: false)
if appstream_util.found()
test('Validate appstream file', appstream_util,
args: ['validate', appstream_file]
)
endif
# Diffuse config file
conf = configuration_data()
conf.set('PKGDATADIR', pkgdatadir)
configure_file(
input: 'diffuserc.in',
output: 'diffuserc',
configuration: conf,
install: true,
install_dir: get_option('sysconfdir')
)
# Data files
install_subdir('usr/share', install_dir: get_option('datadir'), strip_directory: true)
subdir('icons')

View File

@ -10,9 +10,14 @@ finish-args:
- --filesystem=home - --filesystem=home
modules: modules:
- name: diffuse - name: diffuse
builddir: true
buildsystem: meson buildsystem: meson
sources: sources:
- type: git - type: dir
url: https://github.com/MightyCreak/diffuse path: .
branch: v0.6.0 # - type: git
rename-desktop-file: diffuse.desktop # url: file:///home/creak/dev/diffuse
# branch: split-code-into-modules
# - type: git
# url: https://github.com/MightyCreak/diffuse
# branch: v0.6.0

View File

@ -1,29 +1,13 @@
project('diffuse', project('diffuse',
version: '0.6.0', version: '0.7.0',
meson_version: '>= 0.50', meson_version: '>= 0.50',
license: 'GPL-2.0-or-later') license: 'GPL-2.0-or-later',
default_options: [ 'warning_level=2' ])
i18n = import('i18n') i18n = import('i18n')
python = import('python')
py_installation = python.find_installation('python3')
find_program('gtk-update-icon-cache', required: false)
prefix = get_option('prefix')
bindir = prefix / get_option('bindir')
datadir = prefix / get_option('datadir')
localedir = prefix / get_option('localedir')
libexecdir = prefix / get_option('libexecdir')
sysconfdir = prefix / get_option('sysconfdir')
pythondir = py_installation.get_path('purelib')
pkgdatadir = join_paths(datadir, meson.project_name())
podir = join_paths(meson.source_root(), 'po')
subdir('po')
subdir('data') subdir('data')
subdir('src') subdir('src')
subdir('po')
meson.add_install_script('build-scripts/meson-postinstall.py') meson.add_install_script('build-aux/meson/postinstall.py')

0
src/__init__.py Normal file
View File

37
src/diffuse.in Executable file
View File

@ -0,0 +1,37 @@
#!@PYTHON@
# Diffuse: a graphical tool for merging and comparing text files.
#
# Copyright (C) 2019 Derrick Moser <derrick_moser@yahoo.com>
# Copyright (C) 2021 Romain Failliot <romain.failliot@foolstep.com>
#
# 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.
import sys
import gettext
import locale
VERSION = '@VERSION@'
pkgdatadir = '@pkgdatadir@'
localedir = '@localedir@'
sysconfigdir = '@sysconfigdir@'
sys.path.insert(1, pkgdatadir)
gettext.install('diffuse', localedir)
if __name__ == '__main__':
from diffuse import main
sys.exit(main.main(VERSION, sysconfigdir))

414
src/usr/bin/diffuse.py.in → src/main.py Executable file → Normal file
View File

@ -1,121 +1,34 @@
#!/usr/bin/env python3 # Diffuse: a graphical tool for merging and comparing text files.
# -*- coding: utf-8 -*- #
# Copyright (C) 2019 Derrick Moser <derrick_moser@yahoo.com>
# Copyright (C) 2021 Romain Failliot <romain.failliot@foolstep.com>
#
# 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.
# Copyright (C) 2006-2019 Derrick Moser <derrick_moser@yahoo.com>
# Copyright (C) 2015-2020 Romain Failliot <romain.failliot@foolstep.com>
#
# 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. You may also obtain a copy of the GNU General Public License
# from the Free Software Foundation by visiting their web site
# (http://www.fsf.org/) or by writing to the Free Software Foundation, Inc.,
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import codecs
import gettext
import locale
import os import os
import sys import sys
import codecs
# use the program's location as a starting place to search for supporting files import difflib
# such as icon and help documentation import encodings
if hasattr(sys, 'frozen'): import glob
app_path = sys.executable import re
else: import shlex
app_path = os.path.realpath(sys.argv[0]) import stat
bin_dir = os.path.dirname(app_path) import subprocess
import unicodedata
# platform test import webbrowser
def isWindows():
return os.name == 'nt'
# translation location: '../share/locale/<LANG>/LC_MESSAGES/diffuse.mo'
# where '<LANG>' is the language key
lang = locale.getdefaultlocale()[0]
if isWindows():
# gettext looks for the language using environment variables which
# are normally not set on Windows so we try setting it for them
for v in 'LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG':
if v in os.environ:
lang = os.environ[v]
# remove any additional languages, encodings, or modifications
for v in ':.@':
lang = lang.split(v)[0]
break
else:
if lang is not None:
os.environ['LANG'] = lang
del v
locale_dir = 'locale'
else:
locale_dir = '../share/locale'
locale_dir = os.path.join(bin_dir, locale_dir)
gettext.bindtextdomain('diffuse', locale_dir)
gettext.textdomain('diffuse')
_ = gettext.gettext
APP_NAME = 'Diffuse'
VERSION = '0.6.0'
COPYRIGHT = '''{copyright} © 2006-2019 Derrick Moser
{copyright} © 2015-2020 Romain Failliot'''.format(copyright=_("Copyright"))
WEBSITE = 'https://github.com/MightyCreak/diffuse'
# process help options
if __name__ == '__main__':
args = sys.argv
argc = len(args)
if argc == 2 and args[1] in [ '-v', '--version' ]:
print(f'{APP_NAME} {VERSION}\n{COPYRIGHT}')
sys.exit(0)
if argc == 2 and args[1] in [ '-h', '-?', '--help' ]:
print(_('''Usage:
diffuse [ [OPTION...] [FILE...] ]...
diffuse ( -h | -? | --help | -v | --version )
Diffuse is a graphical tool for merging and comparing text files. Diffuse is
able to compare an arbitrary number of files side-by-side and gives users the
ability to manually adjust line matching and directly edit files. Diffuse can
also retrieve revisions of files from Bazaar, CVS, Darcs, Git, Mercurial,
Monotone, RCS, Subversion, and SVK repositories for comparison and merging.
Help Options:
( -h | -? | --help ) Display this usage information
( -v | --version ) Display version and copyright information
Configuration Options:
--no-rcfile Do not read any resource files
--rcfile <file> Specify explicit resource file
General Options:
( -c | --commit ) <rev> File revisions <rev-1> and <rev>
( -D | --close-if-same ) Close all tabs with no differences
( -e | --encoding ) <codec> Use <codec> to read and write files
( -L | --label ) <label> Display <label> instead of the file name
( -m | --modified ) Create a new tab for each modified file
( -r | --revision ) <rev> File revision <rev>
( -s | --separate ) Create a new tab for each file
( -t | --tab ) Start a new tab
( -V | --vcs ) <vcs-list> Version control system search order
--line <line> Start with line <line> selected
--null-file Create a blank file comparison pane
Display Options:
( -b | --ignore-space-change ) Ignore changes to white space
( -B | --ignore-blank-lines ) Ignore changes in blank lines
( -E | --ignore-end-of-line ) Ignore end of line differences
( -i | --ignore-case ) Ignore case differences
( -w | --ignore-all-space ) Ignore white space differences'''))
sys.exit(0)
import gi import gi
@ -137,27 +50,14 @@ from gi.repository import Pango
gi.require_version('PangoCairo', '1.0') gi.require_version('PangoCairo', '1.0')
from gi.repository import PangoCairo from gi.repository import PangoCairo
import difflib
import encodings
import glob
import re
import shlex
import stat
import string
import subprocess
import unicodedata
import webbrowser
from urllib.parse import urlparse from urllib.parse import urlparse
from diffuse import utils
if not hasattr(__builtins__, 'WindowsError'): if not hasattr(__builtins__, 'WindowsError'):
# define 'WindowsError' so 'except' statements will work on all platforms # define 'WindowsError' so 'except' statements will work on all platforms
WindowsError = IOError WindowsError = IOError
# convenience function to display debug messages
def logDebug(s):
pass #sys.stderr.write(f'{APP_NAME}: {s}\n')
# avoid some dictionary lookups when string.whitespace is used in loops # avoid some dictionary lookups when string.whitespace is used in loops
# this is sorted based upon frequency to speed up code for stripping whitespace # this is sorted based upon frequency to speed up code for stripping whitespace
whitespace = ' \t\n\r\x0b\x0c' whitespace = ' \t\n\r\x0b\x0c'
@ -167,27 +67,6 @@ def globEscape(s):
m = dict([ (c, f'[{c}]') for c in '[]?*' ]) m = dict([ (c, f'[{c}]') for c in '[]?*' ])
return ''.join([ m.get(c, c) for c in s ]) return ''.join([ m.get(c, c) for c in s ])
# associate our icon with all of our windows
if __name__ == '__main__':
# this is not automatically set on some older version of PyGTK
Gtk.Window.set_default_icon_name('diffuse')
# convenience class for displaying a message dialogue
class MessageDialog(Gtk.MessageDialog):
def __init__(self, parent, type, s):
if type == Gtk.MessageType.ERROR:
buttons = Gtk.ButtonsType.OK
else:
buttons = Gtk.ButtonsType.OK_CANCEL
Gtk.MessageDialog.__init__(self, parent = parent, destroy_with_parent = True, message_type = type, buttons = buttons, text = s)
self.set_title(APP_NAME)
# report error messages
def logError(s):
m = MessageDialog(None, Gtk.MessageType.ERROR, s)
m.run()
m.destroy()
# colour resources # colour resources
class Colour: class Colour:
def __init__(self, r, g, b, a=1.0): def __init__(self, r, g, b, a=1.0):
@ -544,7 +423,7 @@ class Resources:
try: try:
return self.colours[symbol] return self.colours[symbol]
except KeyError: except KeyError:
logDebug(f'Warning: unknown colour "{symbol}"') utils.logDebug(f'Warning: unknown colour "{symbol}"')
self.colours[symbol] = v = Colour(0.0, 0.0, 0.0) self.colours[symbol] = v = Colour(0.0, 0.0, 0.0)
return v return v
@ -553,7 +432,7 @@ class Resources:
try: try:
return self.floats[symbol] return self.floats[symbol]
except KeyError: except KeyError:
logDebug(f'Warning: unknown float "{symbol}"') utils.logDebug(f'Warning: unknown float "{symbol}"')
self.floats[symbol] = v = 0.5 self.floats[symbol] = v = 0.5
return v return v
@ -562,7 +441,7 @@ class Resources:
try: try:
return self.strings[symbol] return self.strings[symbol]
except KeyError: except KeyError:
logDebug(f'Warning: unknown string "{symbol}"') utils.logDebug(f'Warning: unknown string "{symbol}"')
self.strings[symbol] = v = '' self.strings[symbol] = v = ''
return v return v
@ -687,7 +566,7 @@ class Resources:
pass pass
else: else:
flags = 0 flags = 0
if isWindows(): if utils.isWindows():
flags |= re.IGNORECASE flags |= re.IGNORECASE
self.syntax_file_patterns[key] = re.compile(args[2], flags) self.syntax_file_patterns[key] = re.compile(args[2], flags)
# eg. default to the Python syntax rules when viewing # eg. default to the Python syntax rules when viewing
@ -713,7 +592,7 @@ class Resources:
raise ValueError() raise ValueError()
except: # Grr... the 're' module throws weird errors except: # Grr... the 're' module throws weird errors
#except ValueError: #except ValueError:
logError(_('Error processing line %(line)d of %(file)s.') % { 'line': i + 1, 'file': file_name }) utils.logError(_('Error processing line %(line)d of %(file)s.') % { 'line': i + 1, 'file': file_name })
theResources = Resources() theResources = Resources()
@ -799,7 +678,7 @@ class Preferences:
# find available encodings # find available encodings
self.encodings = sorted(set(encodings.aliases.aliases.values())) self.encodings = sorted(set(encodings.aliases.aliases.values()))
if isWindows(): if utils.isWindows():
svk_bin = 'svk.bat' svk_bin = 'svk.bat'
else: else:
svk_bin = 'svk' svk_bin = 'svk'
@ -860,7 +739,7 @@ class Preferences:
[ 'List', [ 'List',
[ 'Integer', 'tabs_default_panes', 2, _('Default panes'), 2, 16 ], [ 'Integer', 'tabs_default_panes', 2, _('Default panes'), 2, 16 ],
[ 'Boolean', 'tabs_always_show', False, _('Always show the tab bar') ], [ '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') % APP_NAME ] [ 'Boolean', 'tabs_warn_before_quit', True, _('Warn me when closing a tab will quit %s') % utils.APP_NAME ]
], ],
_('Regional Settings'), _('Regional Settings'),
[ 'List', [ 'List',
@ -878,7 +757,7 @@ class Preferences:
'align_ignore_blanklines': ('align_ignore_whitespace', True), 'align_ignore_blanklines': ('align_ignore_whitespace', True),
'align_ignore_endofline': ('align_ignore_whitespace', True) 'align_ignore_endofline': ('align_ignore_whitespace', True)
} }
if isWindows(): if utils.isWindows():
root = os.environ.get('SYSTEMDRIVE', None) root = os.environ.get('SYSTEMDRIVE', None)
if root is None: if root is None:
root = 'C:\\' root = 'C:\\'
@ -914,7 +793,7 @@ class Preferences:
[ 'File', key + '_bin_rlog', 'rlog', _('"rlog" command') ] ]) [ 'File', key + '_bin_rlog', 'rlog', _('"rlog" command') ] ])
else: else:
temp.extend([ [ 'File', key + '_bin', cmd, _('Command') ] ]) temp.extend([ [ 'File', key + '_bin', cmd, _('Command') ] ])
if isWindows(): if utils.isWindows():
temp.append([ 'Boolean', key + '_bash', False, _('Launch from a Bash login shell') ]) temp.append([ 'Boolean', key + '_bash', False, _('Launch from a Bash login shell') ])
if key != 'git': if key != 'git':
temp.append([ 'Boolean', key + '_cygwin', False, _('Update paths for Cygwin') ]) temp.append([ 'Boolean', key + '_cygwin', False, _('Update paths for Cygwin') ])
@ -949,10 +828,10 @@ class Preferences:
except ValueError: except ValueError:
# this may happen if the prefs were written by a # this may happen if the prefs were written by a
# different version -- don't bother the user # different version -- don't bother the user
logDebug(f'Error processing line {j + 1} of {self.path}.') utils.logDebug(f'Error processing line {j + 1} of {self.path}.')
except IOError: except IOError:
# bad $HOME value? -- don't bother the user # bad $HOME value? -- don't bother the user
logDebug(f'Error reading {self.path}.') utils.logDebug(f'Error reading {self.path}.')
# recursively traverses 'template' to discover the preferences and # recursively traverses 'template' to discover the preferences and
# initialise their default values in self.bool_prefs, self.int_prefs, and # initialise their default values in self.bool_prefs, self.int_prefs, and
@ -1021,12 +900,12 @@ class Preferences:
ss.append(f'{k} "{v_escaped}"\n') ss.append(f'{k} "{v_escaped}"\n')
ss.sort() ss.sort()
f = open(self.path, 'w') f = open(self.path, 'w')
f.write(f'# This prefs file was generated by {APP_NAME} {VERSION}.\n\n') f.write(f'# This prefs file was generated by {utils.APP_NAME} {utils.VERSION}.\n\n')
for s in ss: for s in ss:
f.write(s) f.write(s)
f.close() f.close()
except IOError: except IOError:
m = MessageDialog(parent, Gtk.MessageType.ERROR, _('Error writing %s.') % (self.path, )) m = utils.MessageDialog(parent, Gtk.MessageType.ERROR, _('Error writing %s.') % (self.path, ))
m.run() m.run()
m.destroy() m.destroy()
dialog.destroy() dialog.destroy()
@ -1143,7 +1022,7 @@ class Preferences:
# cygwin and native applications can be used on windows, use this method # cygwin and native applications can be used on windows, use this method
# to convert a path to the usual form expected on sys.platform # to convert a path to the usual form expected on sys.platform
def convertToNativePath(self, s): def convertToNativePath(self, s):
if isWindows() and s.find('/') >= 0: if utils.isWindows() and s.find('/') >= 0:
# treat as a cygwin path # treat as a cygwin path
s = s.replace(os.sep, '/') s = s.replace(os.sep, '/')
# convert to a Windows native style path # convert to a Windows native style path
@ -1319,7 +1198,7 @@ def drive_from_path(s):
# constructs a relative path from 'a' to 'b', both should be absolute paths # constructs a relative path from 'a' to 'b', both should be absolute paths
def relpath(a, b): def relpath(a, b):
if isWindows(): if utils.isWindows():
if drive_from_path(a) != drive_from_path(b): if drive_from_path(a) != drive_from_path(b):
return b return b
c1 = [ c for c in a.split(os.sep) if c != '' ] c1 = [ c for c in a.split(os.sep) if c != '' ]
@ -1335,7 +1214,7 @@ def relpath(a, b):
# by prepending './' to the basename # by prepending './' to the basename
def safeRelativePath(abspath1, name, prefs, cygwin_pref): def safeRelativePath(abspath1, name, prefs, cygwin_pref):
s = os.path.join(os.curdir, relpath(abspath1, os.path.abspath(name))) s = os.path.join(os.curdir, relpath(abspath1, os.path.abspath(name)))
if isWindows(): if utils.isWindows():
if prefs.getBool(cygwin_pref): if prefs.getBool(cygwin_pref):
s = s.replace('\\', '/') s = s.replace('\\', '/')
else: else:
@ -1350,12 +1229,12 @@ def bashEscape(s):
def popenRead(dn, cmd, prefs, bash_pref, success_results=None): def popenRead(dn, cmd, prefs, bash_pref, success_results=None):
if success_results is None: if success_results is None:
success_results = [ 0 ] success_results = [ 0 ]
if isWindows() and prefs.getBool(bash_pref): if utils.isWindows() and prefs.getBool(bash_pref):
# launch the command from a bash shell is requested # 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 ])) ] cmd = [ prefs.convertToNativePath('/bin/bash.exe'), '-l', '-c', 'cd {}; {}'.format(bashEscape(dn), ' '.join([ bashEscape(arg) for arg in cmd ])) ]
dn = None dn = None
# use subprocess.Popen to retrieve the file contents # use subprocess.Popen to retrieve the file contents
if isWindows(): if utils.isWindows():
info = subprocess.STARTUPINFO() info = subprocess.STARTUPINFO()
info.dwFlags |= subprocess.STARTF_USESHOWWINDOW info.dwFlags |= subprocess.STARTF_USESHOWWINDOW
info.wShowWindow = subprocess.SW_HIDE info.wShowWindow = subprocess.SW_HIDE
@ -1653,7 +1532,7 @@ class _Cvs:
k0 = k k0 = k
result.append([ (k0, prev), (k, rev) ]) result.append([ (k0, prev), (k, rev) ])
except ValueError: except ValueError:
logError(_('Error parsing revision %s.') % (rev, )) utils.logError(_('Error parsing revision %s.') % (rev, ))
return result return result
def getFolderTemplate(self, prefs, names): def getFolderTemplate(self, prefs, names):
@ -2270,7 +2149,7 @@ class _Rcs:
k0 = k k0 = k
result.append([ (k0, prev), (k, rev) ]) result.append([ (k0, prev), (k, rev) ])
except ValueError: except ValueError:
logError(_('Error parsing revision %s.') % (rev, )) utils.logError(_('Error parsing revision %s.') % (rev, ))
return result return result
def getFolderTemplate(self, prefs, names): def getFolderTemplate(self, prefs, names):
@ -2414,7 +2293,7 @@ class _Svn:
try: try:
prev = self._getPreviousRevision(rev) prev = self._getPreviousRevision(rev)
except ValueError: except ValueError:
logError(_('Error parsing revision %s.') % (rev, )) utils.logError(_('Error parsing revision %s.') % (rev, ))
return result return result
# build command # build command
@ -2599,7 +2478,7 @@ def _get_svk_repo(path, prefs):
name = path name = path
# parse the ~/.svk/config file to discover which directories are part of # parse the ~/.svk/config file to discover which directories are part of
# SVK repositories # SVK repositories
if isWindows(): if utils.isWindows():
name = name.upper() name = name.upper()
svkroot = os.environ.get('SVKROOT', None) svkroot = os.environ.get('SVKROOT', None)
if svkroot is None: if svkroot is None:
@ -2647,7 +2526,7 @@ def _get_svk_repo(path, prefs):
tt.append(key[j]) tt.append(key[j])
j += 1 j += 1
key = ''.join(tt).replace(sep, os.sep) key = ''.join(tt).replace(sep, os.sep)
if isWindows(): if utils.isWindows():
key = key.upper() key = key.upper()
projs.append(key) projs.append(key)
break break
@ -2655,7 +2534,7 @@ def _get_svk_repo(path, prefs):
if _VcsFolderSet(projs).contains(name): if _VcsFolderSet(projs).contains(name):
return _Svk(path) return _Svk(path)
except IOError: except IOError:
logError(_('Error parsing %s.') % (svkconfig, )) utils.logError(_('Error parsing %s.') % (svkconfig, ))
class VCSs: class VCSs:
def __init__(self): def __init__(self):
@ -2912,7 +2791,7 @@ def path2url(path, proto='file'):
for c in s[i:]: for c in s[i:]:
if c == os.sep: if c == os.sep:
c = '/' c = '/'
elif c == ':' and isWindows(): elif c == ':' and utils.isWindows():
c = '|' c = '|'
else: else:
v = ord(c) v = ord(c)
@ -6669,7 +6548,7 @@ class SearchDialog(Gtk.Dialog):
# convenience method to request confirmation when closing the last tab # convenience method to request confirmation when closing the last tab
def confirmTabClose(parent): def confirmTabClose(parent):
dialog = MessageDialog(parent, Gtk.MessageType.WARNING, _('Closing this tab will quit %s.') % (APP_NAME, )) dialog = utils.MessageDialog(parent, Gtk.MessageType.WARNING, _('Closing this tab will quit %s.') % (utils.APP_NAME, ))
end = (dialog.run() == Gtk.ResponseType.OK) end = (dialog.run() == Gtk.ResponseType.OK)
dialog.destroy() dialog.destroy()
return end return end
@ -6756,31 +6635,38 @@ class NumericDialog(Gtk.Dialog):
def url_hook(dialog, link, userdata): def url_hook(dialog, link, userdata):
webbrowser.open(link) webbrowser.open(link)
# the about dialogue
# the about dialog
class AboutDialog(Gtk.AboutDialog): class AboutDialog(Gtk.AboutDialog):
def __init__(self): def __init__(self):
Gtk.AboutDialog.__init__(self) Gtk.AboutDialog.__init__(self)
self.set_logo_icon_name('diffuse') self.set_logo_icon_name('io.github.mightycreak.Diffuse')
if hasattr(self, 'set_program_name'): self.set_program_name(utils.APP_NAME)
# only available in pygtk >= 2.12 self.set_version(utils.VERSION)
self.set_program_name(APP_NAME)
self.set_version(VERSION)
self.set_comments(_('Diffuse is a graphical tool for merging and comparing text files.')) self.set_comments(_('Diffuse is a graphical tool for merging and comparing text files.'))
self.set_copyright(COPYRIGHT) self.set_copyright(utils.COPYRIGHT)
self.set_website(WEBSITE) self.set_website(utils.WEBSITE)
self.set_authors([ 'Derrick Moser <derrick_moser@yahoo.com>', self.set_authors([ 'Derrick Moser <derrick_moser@yahoo.com>',
'Romain Failliot <romain.failliot@foolstep.com>' ]) 'Romain Failliot <romain.failliot@foolstep.com>' ])
self.set_translator_credits(_('translator-credits')) self.set_translator_credits(_('translator-credits'))
ss = [ APP_NAME + ' ' + VERSION + '\n', license_text = [
COPYRIGHT + '\n\n', utils.APP_NAME + ' ' + utils.VERSION + '\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 licence, or (at your option) any later version. utils.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
(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. 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. You may also obtain a copy of the GNU General Public License from the Free Software Foundation by visiting their web site (http://www.fsf.org/) or by writing to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 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.,
self.set_license(''.join(ss)) 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.''') ]
self.set_wrap_license(True) self.set_license(''.join(license_text))
# widget classed to create notebook tabs with labels and a close button # widget classed to create notebook tabs with labels and a close button
# use notebooktab.button.connect() to be notified when the button is pressed # use notebooktab.button.connect() to be notified when the button is pressed
@ -7012,7 +6898,7 @@ class Diffuse(Gtk.Window):
if self.headers[f].has_edits: if self.headers[f].has_edits:
# warn users of any unsaved changes they might lose # 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 = 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(APP_NAME) dialog.set_title(utils.APP_NAME)
dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
dialog.add_button(Gtk.STOCK_NO, Gtk.ResponseType.REJECT) dialog.add_button(Gtk.STOCK_NO, Gtk.ResponseType.REJECT)
dialog.add_button(Gtk.STOCK_YES, Gtk.ResponseType.OK) dialog.add_button(Gtk.STOCK_YES, Gtk.ResponseType.OK)
@ -7124,7 +7010,7 @@ class Diffuse(Gtk.Window):
msg = _('Error reading revision %(rev)s of %(file)s.') % { 'rev': rev, 'file': name } msg = _('Error reading revision %(rev)s of %(file)s.') % { 'rev': rev, 'file': name }
else: else:
msg = _('Error reading %s.') % (name, ) msg = _('Error reading %s.') % (name, )
dialog = MessageDialog(self.get_toplevel(), Gtk.MessageType.ERROR, msg) dialog = utils.MessageDialog(self.get_toplevel(), Gtk.MessageType.ERROR, msg)
dialog.run() dialog.run()
dialog.destroy() dialog.destroy()
return return
@ -7197,7 +7083,7 @@ class Diffuse(Gtk.Window):
else: else:
s = info.name s = info.name
msg = _('The file %s changed on disk. Do you want to reload the file?') % (s, ) msg = _('The file %s changed on disk. Do you want to reload the file?') % (s, )
dialog = MessageDialog(self.get_toplevel(), Gtk.MessageType.QUESTION, msg) dialog = utils.MessageDialog(self.get_toplevel(), Gtk.MessageType.QUESTION, msg)
ok = (dialog.run() == Gtk.ResponseType.OK) ok = (dialog.run() == Gtk.ResponseType.OK)
dialog.destroy() dialog.destroy()
if ok: if ok:
@ -7249,7 +7135,7 @@ class Diffuse(Gtk.Window):
if info.stat[stat.ST_MTIME] < os.stat(name)[stat.ST_MTIME]: if info.stat[stat.ST_MTIME] < os.stat(name)[stat.ST_MTIME]:
msg = _('The file %s has been modified by another process since reading it. If you save, all the external changes could be lost. Save anyways?') % (name, ) msg = _('The file %s has been modified by another process since reading it. If you save, all the external changes could be lost. Save anyways?') % (name, )
if msg is not None: if msg is not None:
dialog = MessageDialog(self.get_toplevel(), Gtk.MessageType.QUESTION, msg) dialog = utils.MessageDialog(self.get_toplevel(), Gtk.MessageType.QUESTION, msg)
end = (dialog.run() != Gtk.ResponseType.OK) end = (dialog.run() != Gtk.ResponseType.OK)
dialog.destroy() dialog.destroy()
if end: if end:
@ -7288,11 +7174,11 @@ class Diffuse(Gtk.Window):
self.setSyntax(syntax) self.setSyntax(syntax)
return True return True
except (UnicodeEncodeError, LookupError): except (UnicodeEncodeError, LookupError):
dialog = MessageDialog(self.get_toplevel(), Gtk.MessageType.ERROR, _('Error encoding to %s.') % (encoding, )) dialog = utils.MessageDialog(self.get_toplevel(), Gtk.MessageType.ERROR, _('Error encoding to %s.') % (encoding, ))
dialog.run() dialog.run()
dialog.destroy() dialog.destroy()
except IOError: except IOError:
dialog = MessageDialog(self.get_toplevel(), Gtk.MessageType.ERROR, _('Error writing %s.') % (name, )) dialog = utils.MessageDialog(self.get_toplevel(), Gtk.MessageType.ERROR, _('Error writing %s.') % (name, ))
dialog.run() dialog.run()
dialog.destroy() dialog.destroy()
return False return False
@ -7558,7 +7444,7 @@ class Diffuse(Gtk.Window):
menuspecs.append([ _('_Help'), [ menuspecs.append([ _('_Help'), [
[_('_Help Contents...'), self.help_contents_cb, None, Gtk.STOCK_HELP, 'help_contents'], [_('_Help Contents...'), self.help_contents_cb, None, Gtk.STOCK_HELP, 'help_contents'],
[], [],
[_('_About %s...') % (APP_NAME, ), self.about_cb, None, Gtk.STOCK_ABOUT, 'about'] ] ]) [_('_About %s...') % (utils.APP_NAME, ), self.about_cb, None, Gtk.STOCK_ABOUT, 'about'] ] ])
# used to disable menu events when switching tabs # used to disable menu events when switching tabs
self.menu_update_depth = 0 self.menu_update_depth = 0
@ -7655,10 +7541,10 @@ class Diffuse(Gtk.Window):
except ValueError: except ValueError:
# this may happen if the state was written by a # this may happen if the state was written by a
# different version -- don't bother the user # different version -- don't bother the user
logDebug(f'Error processing line {j + 1} of {statepath}.') utils.logDebug(f'Error processing line {j + 1} of {statepath}.')
except IOError: except IOError:
# bad $HOME value? -- don't bother the user # bad $HOME value? -- don't bother the user
logDebug(f'Error reading {statepath}.') utils.logDebug(f'Error reading {statepath}.')
self.move(self.int_state['window_x'], self.int_state['window_y']) self.move(self.int_state['window_x'], self.int_state['window_y'])
self.resize(self.int_state['window_width'], self.int_state['window_height']) self.resize(self.int_state['window_width'], self.int_state['window_height'])
@ -7675,13 +7561,13 @@ class Diffuse(Gtk.Window):
ss.append(f'{k} {v}\n') ss.append(f'{k} {v}\n')
ss.sort() ss.sort()
f = open(statepath, 'w') f = open(statepath, 'w')
f.write(f"# This state file was generated by {APP_NAME} {VERSION}.\n\n") f.write(f"# This state file was generated by {utils.APP_NAME} {utils.VERSION}.\n\n")
for s in ss: for s in ss:
f.write(s) f.write(s)
f.close() f.close()
except IOError: except IOError:
# bad $HOME value? -- don't bother the user # bad $HOME value? -- don't bother the user
logDebug(f'Error writing {statepath}.') utils.logDebug(f'Error writing {statepath}.')
# select viewer for a newly selected file in the confirm close dialogue # select viewer for a newly selected file in the confirm close dialogue
def __confirmClose_row_activated_cb(self, tree, path, col, model): def __confirmClose_row_activated_cb(self, tree, path, col, model):
@ -7715,7 +7601,7 @@ class Diffuse(Gtk.Window):
buttons=Gtk.ButtonsType.NONE, buttons=Gtk.ButtonsType.NONE,
text=_('Some files have unsaved changes. Select the files to save before closing.')) text=_('Some files have unsaved changes. Select the files to save before closing.'))
dialog.set_resizable(True) dialog.set_resizable(True)
dialog.set_title(APP_NAME) dialog.set_title(utils.APP_NAME)
# add list of files with unsaved changes # add list of files with unsaved changes
sw = Gtk.ScrolledWindow.new() sw = Gtk.ScrolledWindow.new()
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
@ -7802,7 +7688,7 @@ class Diffuse(Gtk.Window):
# update window's title # update window's title
def updateTitle(self, viewer): def updateTitle(self, viewer):
title = self.notebook.get_tab_label(viewer).get_text() title = self.notebook.get_tab_label(viewer).get_text()
self.set_title(f'{title} - {APP_NAME}') self.set_title(f'{title} - {utils.APP_NAME}')
# update the message in the status bar # update the message in the status bar
def setStatus(self, s): def setStatus(self, s):
@ -7956,7 +7842,7 @@ class Diffuse(Gtk.Window):
viewer.load(i, FileInfo(name, encoding, vcs, rev)) viewer.load(i, FileInfo(name, encoding, vcs, rev))
viewer.setOptions(options) viewer.setOptions(options)
except (IOError, OSError, WindowsError): except (IOError, OSError, WindowsError):
dialog = MessageDialog(self.get_toplevel(), Gtk.MessageType.ERROR, _('Error retrieving commits for %s.') % (dn, )) dialog = utils.MessageDialog(self.get_toplevel(), Gtk.MessageType.ERROR, _('Error retrieving commits for %s.') % (dn, ))
dialog.run() dialog.run()
dialog.destroy() dialog.destroy()
@ -7987,7 +7873,7 @@ class Diffuse(Gtk.Window):
viewer.load(i, FileInfo(name, encoding, vcs, rev)) viewer.load(i, FileInfo(name, encoding, vcs, rev))
viewer.setOptions(options) viewer.setOptions(options)
except (IOError, OSError, WindowsError): except (IOError, OSError, WindowsError):
dialog = MessageDialog(self.get_toplevel(), Gtk.MessageType.ERROR, _('Error retrieving modifications for %s.') % (dn, )) dialog = utils.MessageDialog(self.get_toplevel(), Gtk.MessageType.ERROR, _('Error retrieving modifications for %s.') % (dn, ))
dialog.run() dialog.run()
dialog.destroy() dialog.destroy()
@ -8048,7 +7934,7 @@ class Diffuse(Gtk.Window):
self.notebook.set_current_page(n) self.notebook.set_current_page(n)
self.getCurrentViewer().grab_focus() self.getCurrentViewer().grab_focus()
else: else:
m = MessageDialog(parent, Gtk.MessageType.ERROR, _('No modified files found.')) m = utils.MessageDialog(parent, Gtk.MessageType.ERROR, _('No modified files found.'))
m.run() m.run()
m.destroy() m.destroy()
@ -8068,7 +7954,7 @@ class Diffuse(Gtk.Window):
self.notebook.set_current_page(n) self.notebook.set_current_page(n)
self.getCurrentViewer().grab_focus() self.getCurrentViewer().grab_focus()
else: else:
m = MessageDialog(parent, Gtk.MessageType.ERROR, _('No committed files found.')) m = utils.MessageDialog(parent, Gtk.MessageType.ERROR, _('No committed files found.'))
m.run() m.run()
m.destroy() m.destroy()
@ -8172,7 +8058,7 @@ class Diffuse(Gtk.Window):
msg = _('Phrase not found. Continue from the end of the file?') msg = _('Phrase not found. Continue from the end of the file?')
else: else:
msg = _('Phrase not found. Continue from the start of the file?') msg = _('Phrase not found. Continue from the start of the file?')
dialog = MessageDialog(self.get_toplevel(), Gtk.MessageType.QUESTION, msg) dialog = utils.MessageDialog(self.get_toplevel(), Gtk.MessageType.QUESTION, msg)
dialog.set_default_response(Gtk.ResponseType.OK) dialog.set_default_response(Gtk.ResponseType.OK)
more = (dialog.run() == Gtk.ResponseType.OK) more = (dialog.run() == Gtk.ResponseType.OK)
dialog.destroy() dialog.destroy()
@ -8238,15 +8124,15 @@ class Diffuse(Gtk.Window):
# display help documentation # display help documentation
def help_contents_cb(self, widget, data): def help_contents_cb(self, widget, data):
help_url = None help_url = None
if isWindows(): if utils.isWindows():
# help documentation is distributed as local HTML files # help documentation is distributed as local HTML files
# search for localised manual first # search for localised manual first
parts = [ 'manual' ] parts = [ 'manual' ]
if lang is not None: if utils.lang is not None:
parts = [ 'manual' ] parts = [ 'manual' ]
parts.extend(lang.split('_')) parts.extend(utils.lang.split('_'))
while len(parts) > 0: while len(parts) > 0:
help_file = os.path.join(bin_dir, '_'.join(parts) + '.html') help_file = os.path.join(utils.bin_dir, '_'.join(parts) + '.html')
if os.path.isfile(help_file): if os.path.isfile(help_file):
# we found a help file # we found a help file
help_url = path2url(help_file) help_url = path2url(help_file)
@ -8264,11 +8150,11 @@ class Diffuse(Gtk.Window):
break break
if browser is not None: if browser is not None:
# find localised help file # find localised help file
if lang is None: if utils.lang is None:
parts = [] parts = []
else: else:
parts = lang.split('_') parts = utils.lang.split('_')
s = os.path.abspath(os.path.join(bin_dir, '../share/gnome/help/diffuse')) s = os.path.abspath(os.path.join(utils.bin_dir, '../share/gnome/help/diffuse'))
while True: while True:
if len(parts) > 0: if len(parts) > 0:
d = '_'.join(parts) d = '_'.join(parts)
@ -8286,10 +8172,10 @@ class Diffuse(Gtk.Window):
del parts[-1] del parts[-1]
if help_url is None: if help_url is None:
# no local help file is available, show on-line help # no local help file is available, show on-line help
help_url = WEBSITE + 'manual.html' help_url = utils.WEBSITE + 'manual.html'
# ask for localised manual # ask for localised manual
if lang is not None: if utils.lang is not None:
help_url += '?lang=' + lang help_url += '?lang=' + utils.lang
# use a web browser to display the help documentation # use a web browser to display the help documentation
webbrowser.open(help_url) webbrowser.open(help_url)
@ -8307,31 +8193,70 @@ 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', 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, ()) GObject.signal_new('save-as', Diffuse.FileDiffViewer.PaneHeader, GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ())
# create nested subdirectories and return the complete path def main(version, sysconfigdir):
def make_subdirs(p, ss): # app = Application()
for s in ss: # return app.run(sys.argv)
p = os.path.join(p, s)
if not os.path.exists(p): utils.VERSION = version
try:
os.mkdir(p) args = sys.argv
except IOError: argc = len(args)
pass
return p if argc == 2 and args[1] in [ '-v', '--version' ]:
print('%s %s\n%s' % (utils.APP_NAME, utils.VERSION, utils.COPYRIGHT))
sys.exit(0)
if argc == 2 and args[1] in [ '-h', '-?', '--help' ]:
print(_('''Usage:
diffuse [ [OPTION...] [FILE...] ]...
diffuse ( -h | -? | --help | -v | --version )
Diffuse is a graphical tool for merging and comparing text files. Diffuse is
able to compare an arbitrary number of files side-by-side and gives users the
ability to manually adjust line matching and directly edit files. Diffuse can
also retrieve revisions of files from Bazaar, CVS, Darcs, Git, Mercurial,
Monotone, RCS, Subversion, and SVK repositories for comparison and merging.
Help Options:
( -h | -? | --help ) Display this usage information
( -v | --version ) Display version and copyright information
Configuration Options:
--no-rcfile Do not read any resource files
--rcfile <file> Specify explicit resource file
General Options:
( -c | --commit ) <rev> File revisions <rev-1> and <rev>
( -D | --close-if-same ) Close all tabs with no differences
( -e | --encoding ) <codec> Use <codec> to read and write files
( -L | --label ) <label> Display <label> instead of the file name
( -m | --modified ) Create a new tab for each modified file
( -r | --revision ) <rev> File revision <rev>
( -s | --separate ) Create a new tab for each file
( -t | --tab ) Start a new tab
( -V | --vcs ) <vcs-list> Version control system search order
--line <line> Start with line <line> selected
--null-file Create a blank file comparison pane
Display Options:
( -b | --ignore-space-change ) Ignore changes to white space
( -B | --ignore-blank-lines ) Ignore changes in blank lines
( -E | --ignore-end-of-line ) Ignore end of line differences
( -i | --ignore-case ) Ignore case differences
( -w | --ignore-all-space ) Ignore white space differences'''))
sys.exit(0)
# process the command line arguments
if __name__ == '__main__':
# find the config directory and create it if it didn't exist # find the config directory and create it if it didn't exist
rc_dir, subdirs = os.environ.get('XDG_CONFIG_HOME', None), ['diffuse'] rc_dir, subdirs = os.environ.get('XDG_CONFIG_HOME', None), ['diffuse']
if rc_dir is None: if rc_dir is None:
rc_dir = os.path.expanduser('~') rc_dir = os.path.expanduser('~')
subdirs.insert(0, '.config') subdirs.insert(0, '.config')
rc_dir = make_subdirs(rc_dir, subdirs) rc_dir = utils.make_subdirs(rc_dir, subdirs)
# find the local data directory and create it if it didn't exist # find the local data directory and create it if it didn't exist
data_dir, subdirs = os.environ.get('XDG_DATA_HOME', None), ['diffuse'] data_dir, subdirs = os.environ.get('XDG_DATA_HOME', None), ['diffuse']
if data_dir is None: if data_dir is None:
data_dir = os.path.expanduser('~') data_dir = os.path.expanduser('~')
subdirs[:0] = [ '.local', 'share' ] subdirs[:0] = [ '.local', 'share' ]
data_dir = make_subdirs(data_dir, subdirs) data_dir = utils.make_subdirs(data_dir, subdirs)
# load resource files # load resource files
i, rc_files = 1, [] i, rc_files = 1, []
if i < argc and args[i] == '--no-rcfile': if i < argc and args[i] == '--no-rcfile':
@ -8342,10 +8267,10 @@ if __name__ == '__main__':
i += 1 i += 1
else: else:
# parse system wide then personal initialisation files # parse system wide then personal initialisation files
if isWindows(): if utils.isWindows():
rc_file = os.path.join(bin_dir, 'diffuserc') rc_file = os.path.join(utils.bin_dir, 'diffuserc')
else: else:
rc_file = os.path.join(bin_dir, '@SYSCONFIGDIR@/diffuserc') rc_file = os.path.join(utils.bin_dir, f'{sysconfigdir}/diffuserc')
for rc_file in rc_file, os.path.join(rc_dir, 'diffuserc'): for rc_file in rc_file, os.path.join(rc_dir, 'diffuserc'):
if os.path.isfile(rc_file): if os.path.isfile(rc_file):
rc_files.append(rc_file) rc_files.append(rc_file)
@ -8354,11 +8279,14 @@ if __name__ == '__main__':
# reported with normalised file names # reported with normalised file names
rc_file = os.path.abspath(rc_file) rc_file = os.path.abspath(rc_file)
try: try:
# diffuse.theResources.parse(rc_file) # Modularization
theResources.parse(rc_file) theResources.parse(rc_file)
except IOError: except IOError:
logError(_('Error reading %s.') % (rc_file, )) utils.logError(_('Error reading %s.') % (rc_file, ))
# diff = diffuse.Diffuse(rc_dir) # Modularization
diff = Diffuse(rc_dir) diff = Diffuse(rc_dir)
# load state # load state
statepath = os.path.join(data_dir, 'state') statepath = os.path.join(data_dir, 'state')
diff.loadState(statepath) diff.loadState(statepath)
@ -8437,7 +8365,7 @@ if __name__ == '__main__':
try: try:
options['line'] = int(args[i]) options['line'] = int(args[i])
except ValueError: except ValueError:
logError(_('Error parsing line number.')) utils.logError(_('Error parsing line number.'))
elif arg == '--null-file': elif arg == '--null-file':
# add a blank file pane # add a blank file pane
if mode == 'single' or mode == 'separate': if mode == 'single' or mode == 'separate':
@ -8447,14 +8375,14 @@ if __name__ == '__main__':
revs = [] revs = []
had_specs = True had_specs = True
else: else:
logError(_('Skipping unknown argument "%s".') % (args[i], )) utils.logError(_('Skipping unknown argument "%s".') % (args[i], ))
else: else:
filename = diff.prefs.convertToNativePath(args[i]) filename = diff.prefs.convertToNativePath(args[i])
if (mode == 'single' or mode == 'separate') and os.path.isdir(filename): if (mode == 'single' or mode == 'separate') and os.path.isdir(filename):
if len(specs) > 0: if len(specs) > 0:
filename = os.path.join(filename, os.path.basename(specs[-1][0])) filename = os.path.join(filename, os.path.basename(specs[-1][0]))
else: else:
logError(_('Error processing argument "%s". Directory not expected.') % (args[i], )) utils.logError(_('Error processing argument "%s". Directory not expected.') % (args[i], ))
filename = None filename = None
if filename is not None: if filename is not None:
if len(revs) == 0: if len(revs) == 0:
@ -8483,3 +8411,5 @@ if __name__ == '__main__':
Gtk.main() Gtk.main()
# save state # save state
diff.saveState(statepath) diff.saveState(statepath)
return 0

View File

@ -1,42 +1,28 @@
# Diffuse binary file pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name())
diffuse_conf = configuration_data() moduledir = join_paths(pkgdatadir, meson.project_name())
diffuse_conf.set('SYSCONFIGDIR', sysconfdir) sysconfdir = join_paths(get_option('prefix'), get_option('sysconfdir'))
python = import('python')
conf = configuration_data()
conf.set('PYTHON', python.find_installation('python3').path())
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)
configure_file( configure_file(
input: 'usr/bin/diffuse.py.in', input: 'diffuse.in',
output: 'diffuse', output: 'diffuse',
configuration: diffuse_conf, configuration: conf,
install: true, install: true,
install_dir: bindir install_dir: get_option('bindir')
) )
# Diffuse config file diffuse_sources = [
diffuserc_conf = configuration_data() '__init__.py',
diffuserc_conf.set('PKGDATADIR', pkgdatadir) 'main.py',
'utils.py',
]
configure_file( install_data(diffuse_sources, install_dir: moduledir)
input: 'etc/diffuserc.py.in',
output: 'diffuserc',
configuration: diffuserc_conf,
install: true,
install_dir: sysconfdir
)
# Validate MetaInfo file
metainfo_file = join_paths(meson.source_root(), 'src/usr/share/metainfo/io.github.mightycreak.Diffuse.metainfo.xml')
ascli_exe = find_program('appstreamcli', required: false)
if ascli_exe.found()
test(
'validate metainfo file',
ascli_exe,
args: [
'validate',
'--no-net',
'--pedantic',
metainfo_file
]
)
endif
# Data files
install_subdir('usr/share', install_dir: datadir, strip_directory: true)

93
src/utils.py Normal file
View File

@ -0,0 +1,93 @@
# Diffuse: a graphical tool for merging and comparing text files.
#
# Copyright (C) 2019 Derrick Moser <derrick_moser@yahoo.com>
# Copyright (C) 2021 Romain Failliot <romain.failliot@foolstep.com>
#
# 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.
import os
import sys
import locale
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# convenience class for displaying a message dialogue
class MessageDialog(Gtk.MessageDialog):
def __init__(self, parent, type, s):
if type == Gtk.MessageType.ERROR:
buttons = Gtk.ButtonsType.OK
else:
buttons = Gtk.ButtonsType.OK_CANCEL
Gtk.MessageDialog.__init__(self, parent = parent, destroy_with_parent = True, message_type = type, buttons = buttons, text = s)
self.set_title(APP_NAME)
# platform test
def isWindows():
return os.name == 'nt'
# convenience function to display debug messages
def logDebug(s):
pass #sys.stderr.write(f'{APP_NAME}: {s}\n')
# report error messages
def logError(s):
m = MessageDialog(None, Gtk.MessageType.ERROR, s)
m.run()
m.destroy()
# create nested subdirectories and return the complete path
def make_subdirs(p, ss):
for s in ss:
p = os.path.join(p, s)
if not os.path.exists(p):
try:
os.mkdir(p)
except IOError:
pass
return p
# use the program's location as a starting place to search for supporting files
# such as icon and help documentation
if hasattr(sys, 'frozen'):
app_path = sys.executable
else:
app_path = os.path.realpath(sys.argv[0])
bin_dir = os.path.dirname(app_path)
# translation location: '../share/locale/<LANG>/LC_MESSAGES/diffuse.mo'
# where '<LANG>' is the language key
lang = locale.getdefaultlocale()[0]
if isWindows():
# gettext looks for the language using environment variables which
# are normally not set on Windows so we try setting it for them
for v in 'LC_ALL', 'LC_CTYPE', 'LANG', 'LANGUAGE':
if v in os.environ:
lang = os.environ[v]
# remove any additional languages, encodings, or modifications
for v in ':.@':
lang = lang.split(v)[0]
break
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/'

View File

@ -197,7 +197,7 @@ for lang in os.listdir(d):
j = s.find(';', i) j = s.find(';', i)
a.append(unichr(int(s[i:j]))) a.append(unichr(int(s[i:j])))
idx = j + 1 idx = j + 1
s = u''.join(a) s = ''.join(a)
s = codecs.encode(s, 'utf-8') s = codecs.encode(s, 'utf-8')
# clean up translator credit portion # clean up translator credit portion
div = extract_tag(s, '<div class="othercredit">', '</div>') div = extract_tag(s, '<div class="othercredit">', '</div>')