Start modularizing the code

* The entry point (`__main__`) is in diffuxe.in
* The main (almost all) code is now in main.py
* Some util functions and variables are in utils.py
* Following the same folder structure as with a new project with GNOME
  Builder:
    - `src/` is now just for the code itself
    - `data/` is for the other files (metainfo, desktop, config, ...)
    - `po/` for the translations
    - The Desktop file is renamed with the app ID
    - The `meson.build` files are closer to what GNOME Builder generates
    - More tests for the package (appstream and desktop)
    - Almost all the files in `etc/`, `usr/` are properly handled by
      meson now
    - Just use `gettext.install()` to initialize gettext
    - Remove call to `Gtk.Window.set_default_icon_name()`
* Website now points to https://mightycreak.github.io/diffuse/
This commit is contained in:
Romain Failliot 2020-03-29 10:08:15 -04:00
parent 0ce1a09974
commit fd3c2bfb92
66 changed files with 737 additions and 596 deletions

View File

@ -2,7 +2,7 @@
name: CI
# Controls when the action will run.
# Controls when the action will run.
on:
# Triggers the workflow on push or pull request events but only for the master branch
push:
@ -26,12 +26,15 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- name: Meson Build
uses: BSFishy/meson-build@v1.0.1
- name: Install dependencies
run: sudo apt-get -y install appstream appstream-util desktop-file-utils gettext
- name: Meson build
uses: BSFishy/meson-build@v1.0.3
with:
action: build
- name: Meson Test
uses: BSFishy/meson-build@v1.0.1
- name: Meson test
uses: BSFishy/meson-build@v1.0.3
with:
action: test

3
.gitignore vendored
View File

@ -13,8 +13,7 @@ __pycache__/
# Distribution / packaging
.Python
build/
builddir/
builddir-flatpak/
build-flatpak/
develop-eggs/
dist/
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 MetaInfo file
- New SVG icon (thanks @creepertron95, @jimmac and @freddii)
- Started modularizing the code
### Changed
- 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
### Fixed
- Fixed some GTK deprecation warnings
## [0.6.0] - 2020-11-29

View File

@ -20,48 +20,71 @@ Some key features of Diffuse:
## Requirements
Diffuse is implemented entirely in Python and should run on any platform with
Python and PyGObject.
* Python >= 3.4
* PyGObject >= 3.18
Diffuse is implemented entirely in Python and should run on any platform with
Python and PyGTK. If you need to manually install PyGTK, please be aware its
dependencies should be installed prior to installing PyGTK.
## Users
Diffuse can be run directly from an untared source distribution on any POSIX
system or installed with the instructions described in the next section.
### Installing using Flatpak
The location of the personal preferences, state, and initialisation files have
changed in the 0.4.1 release. Old settings may be migrated using the following
commands:
This is the easiest way to install Diffuse:
$ mkdir -p ~/.config/diffuse
$ mv ~/.diffuse/config ~/.config/diffuse/state
$ mv ~/.diffuse/* ~/.config/diffuse
$ rmdir ~/.diffuse
```sh
flatpak install io.github.mightycreak.Diffuse
```
The rules for parsing files in `~/.diffuse` changed in the 0.3.0 release.
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`.
## Developers
## 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.
To install diffuse locally:
meson builddir
meson install -C builddir
```sh
meson setup build
cd build
meson compile
meson install # requires admin privileges
```
To uninstall diffuse afterwards:
sudo ninja uninstall -C builddir
sudo rm -v /usr/local/share/locale/*/LC_MESSAGES/diffuse.mo
```sh
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
[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
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
writable directory on the pen drive).
## Installing the Flatpak package
flatpak install io.github.mightycreak.Diffuse
## Building and testing the Flatpak package
To install Diffuse locally:
flatpak install flatpak install runtime/org.gnome.Sdk/$(uname -p)/3.38
flatpak-builder builddir-flatpak --user --install io.github.mightycreak.Diffuse.yml
```sh
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:
flatpak run io.github.mightycreak.Diffuse
```sh
flatpak run io.github.mightycreak.Diffuse
```
To uninstall Diffuse:
flatpak remove io.github.mightycreak.Diffuse
```sh
flatpak remove io.github.mightycreak.Diffuse
```
## Help Documentation

View File

@ -1,7 +1,5 @@
#!/usr/bin/env python3
import sysconfig
from compileall import compile_dir
from os import environ, path
from subprocess import call
@ -13,3 +11,9 @@ destdir = environ.get('DESTDIR', '')
if not destdir:
print('Updating icon cache...')
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"?>
<component type="desktop-application">
<id>io.github.mightycreak.Diffuse</id>
<name>Diffuse Merge Tool</name>
<summary>Graphical tool for merging and comparing text files</summary>
<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.
</p>
</description>
<metadata_license>FSFAP</metadata_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>
<url type="homepage">https://mightycreak.github.io/diffuse/</url>
<url type="bugtracker">https://github.com/MightyCreak/diffuse/issues</url>
<screenshots>
<screenshot type="default">
<image>https://mightycreak.github.io/diffuse/images/screenshot_v0.5.0.png</image>
<caption>Main window: diff between two files</caption>
<image>https://mightycreak.github.io/diffuse/images/screenshot_v0.7.0.png</image>
</screenshot>
</screenshots>
<releases>
<release version="0.6.0" date="2020-11-29">
<description>
@ -102,7 +96,6 @@
</description>
</release>
</releases>
<developer_name>Romain Failliot</developer_name>
<update_contact>romain.failliot@foolstep.com</update_contact>
</component>

View File

@ -1,10 +1,49 @@
desktop_file = 'diffuse.desktop'
i18n.merge_file(
desktop_file,
input: desktop_file + '.in',
output: desktop_file,
pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name())
desktop_file = i18n.merge_file(
input: 'io.github.mightycreak.Diffuse.desktop.in',
output: 'io.github.mightycreak.Diffuse.desktop',
type: 'desktop',
po_dir: '../po',
install: true,
install_dir: join_paths(datadir, 'applications'),
type: 'desktop'
install_dir: join_paths(get_option('datadir'), 'applications')
)
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
modules:
- name: diffuse
builddir: true
buildsystem: meson
sources:
- type: git
url: https://github.com/MightyCreak/diffuse
branch: v0.6.0
rename-desktop-file: diffuse.desktop
- type: dir
path: .
# - type: git
# 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',
version: '0.6.0',
version: '0.7.0',
meson_version: '>= 0.50',
license: 'GPL-2.0-or-later')
license: 'GPL-2.0-or-later',
default_options: [ 'warning_level=2' ])
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('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
# -*- coding: utf-8 -*-
# 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.
# 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 sys
# 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)
# platform test
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 codecs
import difflib
import encodings
import glob
import re
import shlex
import stat
import subprocess
import unicodedata
import webbrowser
import gi
@ -137,27 +50,14 @@ from gi.repository import Pango
gi.require_version('PangoCairo', '1.0')
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 diffuse import utils
if not hasattr(__builtins__, 'WindowsError'):
# define 'WindowsError' so 'except' statements will work on all platforms
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
# this is sorted based upon frequency to speed up code for stripping whitespace
whitespace = ' \t\n\r\x0b\x0c'
@ -167,27 +67,6 @@ def globEscape(s):
m = dict([ (c, f'[{c}]') for c in '[]?*' ])
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
class Colour:
def __init__(self, r, g, b, a=1.0):
@ -544,7 +423,7 @@ class Resources:
try:
return self.colours[symbol]
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)
return v
@ -553,7 +432,7 @@ class Resources:
try:
return self.floats[symbol]
except KeyError:
logDebug(f'Warning: unknown float "{symbol}"')
utils.logDebug(f'Warning: unknown float "{symbol}"')
self.floats[symbol] = v = 0.5
return v
@ -562,7 +441,7 @@ class Resources:
try:
return self.strings[symbol]
except KeyError:
logDebug(f'Warning: unknown string "{symbol}"')
utils.logDebug(f'Warning: unknown string "{symbol}"')
self.strings[symbol] = v = ''
return v
@ -687,7 +566,7 @@ class Resources:
pass
else:
flags = 0
if isWindows():
if utils.isWindows():
flags |= re.IGNORECASE
self.syntax_file_patterns[key] = re.compile(args[2], flags)
# eg. default to the Python syntax rules when viewing
@ -713,7 +592,7 @@ class Resources:
raise ValueError()
except: # Grr... the 're' module throws weird errors
#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()
@ -799,7 +678,7 @@ class Preferences:
# find available encodings
self.encodings = sorted(set(encodings.aliases.aliases.values()))
if isWindows():
if utils.isWindows():
svk_bin = 'svk.bat'
else:
svk_bin = 'svk'
@ -860,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') % APP_NAME ]
[ 'Boolean', 'tabs_warn_before_quit', True, _('Warn me when closing a tab will quit %s') % utils.APP_NAME ]
],
_('Regional Settings'),
[ 'List',
@ -878,7 +757,7 @@ class Preferences:
'align_ignore_blanklines': ('align_ignore_whitespace', True),
'align_ignore_endofline': ('align_ignore_whitespace', True)
}
if isWindows():
if utils.isWindows():
root = os.environ.get('SYSTEMDRIVE', None)
if root is None:
root = 'C:\\'
@ -914,7 +793,7 @@ class Preferences:
[ 'File', key + '_bin_rlog', 'rlog', _('"rlog" command') ] ])
else:
temp.extend([ [ 'File', key + '_bin', cmd, _('Command') ] ])
if isWindows():
if utils.isWindows():
temp.append([ 'Boolean', key + '_bash', False, _('Launch from a Bash login shell') ])
if key != 'git':
temp.append([ 'Boolean', key + '_cygwin', False, _('Update paths for Cygwin') ])
@ -949,10 +828,10 @@ class Preferences:
except ValueError:
# this may happen if the prefs were written by a
# 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:
# 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
# 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.sort()
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:
f.write(s)
f.close()
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.destroy()
dialog.destroy()
@ -1143,7 +1022,7 @@ class Preferences:
# cygwin and native applications can be used on windows, use this method
# to convert a path to the usual form expected on sys.platform
def convertToNativePath(self, s):
if isWindows() and s.find('/') >= 0:
if utils.isWindows() and s.find('/') >= 0:
# treat as a cygwin path
s = s.replace(os.sep, '/')
# 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
def relpath(a, b):
if isWindows():
if utils.isWindows():
if drive_from_path(a) != drive_from_path(b):
return b
c1 = [ c for c in a.split(os.sep) if c != '' ]
@ -1335,7 +1214,7 @@ def relpath(a, b):
# by prepending './' to the basename
def safeRelativePath(abspath1, name, prefs, cygwin_pref):
s = os.path.join(os.curdir, relpath(abspath1, os.path.abspath(name)))
if isWindows():
if utils.isWindows():
if prefs.getBool(cygwin_pref):
s = s.replace('\\', '/')
else:
@ -1350,12 +1229,12 @@ def bashEscape(s):
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):
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 isWindows():
if utils.isWindows():
info = subprocess.STARTUPINFO()
info.dwFlags |= subprocess.STARTF_USESHOWWINDOW
info.wShowWindow = subprocess.SW_HIDE
@ -1653,7 +1532,7 @@ class _Cvs:
k0 = k
result.append([ (k0, prev), (k, rev) ])
except ValueError:
logError(_('Error parsing revision %s.') % (rev, ))
utils.logError(_('Error parsing revision %s.') % (rev, ))
return result
def getFolderTemplate(self, prefs, names):
@ -2270,7 +2149,7 @@ class _Rcs:
k0 = k
result.append([ (k0, prev), (k, rev) ])
except ValueError:
logError(_('Error parsing revision %s.') % (rev, ))
utils.logError(_('Error parsing revision %s.') % (rev, ))
return result
def getFolderTemplate(self, prefs, names):
@ -2414,7 +2293,7 @@ class _Svn:
try:
prev = self._getPreviousRevision(rev)
except ValueError:
logError(_('Error parsing revision %s.') % (rev, ))
utils.logError(_('Error parsing revision %s.') % (rev, ))
return result
# build command
@ -2599,7 +2478,7 @@ def _get_svk_repo(path, prefs):
name = path
# parse the ~/.svk/config file to discover which directories are part of
# SVK repositories
if isWindows():
if utils.isWindows():
name = name.upper()
svkroot = os.environ.get('SVKROOT', None)
if svkroot is None:
@ -2647,7 +2526,7 @@ def _get_svk_repo(path, prefs):
tt.append(key[j])
j += 1
key = ''.join(tt).replace(sep, os.sep)
if isWindows():
if utils.isWindows():
key = key.upper()
projs.append(key)
break
@ -2655,7 +2534,7 @@ def _get_svk_repo(path, prefs):
if _VcsFolderSet(projs).contains(name):
return _Svk(path)
except IOError:
logError(_('Error parsing %s.') % (svkconfig, ))
utils.logError(_('Error parsing %s.') % (svkconfig, ))
class VCSs:
def __init__(self):
@ -2912,7 +2791,7 @@ def path2url(path, proto='file'):
for c in s[i:]:
if c == os.sep:
c = '/'
elif c == ':' and isWindows():
elif c == ':' and utils.isWindows():
c = '|'
else:
v = ord(c)
@ -6669,7 +6548,7 @@ class SearchDialog(Gtk.Dialog):
# convenience method to request confirmation when closing the last tab
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)
dialog.destroy()
return end
@ -6756,31 +6635,38 @@ class NumericDialog(Gtk.Dialog):
def url_hook(dialog, link, userdata):
webbrowser.open(link)
# the about dialogue
# the about dialog
class AboutDialog(Gtk.AboutDialog):
def __init__(self):
Gtk.AboutDialog.__init__(self)
self.set_logo_icon_name('diffuse')
if hasattr(self, 'set_program_name'):
# only available in pygtk >= 2.12
self.set_program_name(APP_NAME)
self.set_version(VERSION)
self.set_logo_icon_name('io.github.mightycreak.Diffuse')
self.set_program_name(utils.APP_NAME)
self.set_version(utils.VERSION)
self.set_comments(_('Diffuse is a graphical tool for merging and comparing text files.'))
self.set_copyright(COPYRIGHT)
self.set_website(WEBSITE)
self.set_copyright(utils.COPYRIGHT)
self.set_website(utils.WEBSITE)
self.set_authors([ 'Derrick Moser <derrick_moser@yahoo.com>',
'Romain Failliot <romain.failliot@foolstep.com>' ])
self.set_translator_credits(_('translator-credits'))
ss = [ APP_NAME + ' ' + VERSION + '\n',
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 licence, or (at your option) any later version.
license_text = [
utils.APP_NAME + ' ' + utils.VERSION + '\n\n',
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
""") ]
self.set_license(''.join(ss))
self.set_wrap_license(True)
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.''') ]
self.set_license(''.join(license_text))
# widget classed to create notebook tabs with labels and a close button
# 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:
# 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(APP_NAME)
dialog.set_title(utils.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)
@ -7124,7 +7010,7 @@ class Diffuse(Gtk.Window):
msg = _('Error reading revision %(rev)s of %(file)s.') % { 'rev': rev, 'file': name }
else:
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.destroy()
return
@ -7197,7 +7083,7 @@ class Diffuse(Gtk.Window):
else:
s = info.name
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)
dialog.destroy()
if ok:
@ -7249,7 +7135,7 @@ class Diffuse(Gtk.Window):
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, )
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)
dialog.destroy()
if end:
@ -7288,11 +7174,11 @@ class Diffuse(Gtk.Window):
self.setSyntax(syntax)
return True
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.destroy()
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.destroy()
return False
@ -7558,7 +7444,7 @@ class Diffuse(Gtk.Window):
menuspecs.append([ _('_Help'), [
[_('_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
self.menu_update_depth = 0
@ -7655,10 +7541,10 @@ class Diffuse(Gtk.Window):
except ValueError:
# this may happen if the state was written by a
# 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:
# 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.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.sort()
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:
f.write(s)
f.close()
except IOError:
# 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
def __confirmClose_row_activated_cb(self, tree, path, col, model):
@ -7715,7 +7601,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(APP_NAME)
dialog.set_title(utils.APP_NAME)
# add list of files with unsaved changes
sw = Gtk.ScrolledWindow.new()
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
@ -7802,7 +7688,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} - {APP_NAME}')
self.set_title(f'{title} - {utils.APP_NAME}')
# update the message in the status bar
def setStatus(self, s):
@ -7956,7 +7842,7 @@ class Diffuse(Gtk.Window):
viewer.load(i, FileInfo(name, encoding, vcs, rev))
viewer.setOptions(options)
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.destroy()
@ -7987,7 +7873,7 @@ class Diffuse(Gtk.Window):
viewer.load(i, FileInfo(name, encoding, vcs, rev))
viewer.setOptions(options)
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.destroy()
@ -8048,7 +7934,7 @@ class Diffuse(Gtk.Window):
self.notebook.set_current_page(n)
self.getCurrentViewer().grab_focus()
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.destroy()
@ -8068,7 +7954,7 @@ class Diffuse(Gtk.Window):
self.notebook.set_current_page(n)
self.getCurrentViewer().grab_focus()
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.destroy()
@ -8172,7 +8058,7 @@ class Diffuse(Gtk.Window):
msg = _('Phrase not found. Continue from the end of the file?')
else:
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)
more = (dialog.run() == Gtk.ResponseType.OK)
dialog.destroy()
@ -8238,15 +8124,15 @@ class Diffuse(Gtk.Window):
# display help documentation
def help_contents_cb(self, widget, data):
help_url = None
if isWindows():
if utils.isWindows():
# help documentation is distributed as local HTML files
# search for localised manual first
parts = [ 'manual' ]
if lang is not None:
if utils.lang is not None:
parts = [ 'manual' ]
parts.extend(lang.split('_'))
parts.extend(utils.lang.split('_'))
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):
# we found a help file
help_url = path2url(help_file)
@ -8264,11 +8150,11 @@ class Diffuse(Gtk.Window):
break
if browser is not None:
# find localised help file
if lang is None:
if utils.lang is None:
parts = []
else:
parts = lang.split('_')
s = os.path.abspath(os.path.join(bin_dir, '../share/gnome/help/diffuse'))
parts = utils.lang.split('_')
s = os.path.abspath(os.path.join(utils.bin_dir, '../share/gnome/help/diffuse'))
while True:
if len(parts) > 0:
d = '_'.join(parts)
@ -8286,10 +8172,10 @@ class Diffuse(Gtk.Window):
del parts[-1]
if help_url is None:
# 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
if lang is not None:
help_url += '?lang=' + lang
if utils.lang is not None:
help_url += '?lang=' + utils.lang
# use a web browser to display the help documentation
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-as', Diffuse.FileDiffViewer.PaneHeader, GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ())
# 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
def main(version, sysconfigdir):
# 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))
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
rc_dir, subdirs = os.environ.get('XDG_CONFIG_HOME', None), ['diffuse']
if rc_dir is None:
rc_dir = os.path.expanduser('~')
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
data_dir, subdirs = os.environ.get('XDG_DATA_HOME', None), ['diffuse']
if data_dir is None:
data_dir = os.path.expanduser('~')
subdirs[:0] = [ '.local', 'share' ]
data_dir = make_subdirs(data_dir, subdirs)
data_dir = utils.make_subdirs(data_dir, subdirs)
# load resource files
i, rc_files = 1, []
if i < argc and args[i] == '--no-rcfile':
@ -8342,10 +8267,10 @@ if __name__ == '__main__':
i += 1
else:
# parse system wide then personal initialisation files
if isWindows():
rc_file = os.path.join(bin_dir, 'diffuserc')
if utils.isWindows():
rc_file = os.path.join(utils.bin_dir, 'diffuserc')
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'):
if os.path.isfile(rc_file):
rc_files.append(rc_file)
@ -8354,11 +8279,14 @@ if __name__ == '__main__':
# reported with normalised file names
rc_file = os.path.abspath(rc_file)
try:
# diffuse.theResources.parse(rc_file) # Modularization
theResources.parse(rc_file)
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)
# load state
statepath = os.path.join(data_dir, 'state')
diff.loadState(statepath)
@ -8437,7 +8365,7 @@ if __name__ == '__main__':
try:
options['line'] = int(args[i])
except ValueError:
logError(_('Error parsing line number.'))
utils.logError(_('Error parsing line number.'))
elif arg == '--null-file':
# add a blank file pane
if mode == 'single' or mode == 'separate':
@ -8447,14 +8375,14 @@ if __name__ == '__main__':
revs = []
had_specs = True
else:
logError(_('Skipping unknown argument "%s".') % (args[i], ))
utils.logError(_('Skipping unknown argument "%s".') % (args[i], ))
else:
filename = diff.prefs.convertToNativePath(args[i])
if (mode == 'single' or mode == 'separate') and os.path.isdir(filename):
if len(specs) > 0:
filename = os.path.join(filename, os.path.basename(specs[-1][0]))
else:
logError(_('Error processing argument "%s". Directory not expected.') % (args[i], ))
utils.logError(_('Error processing argument "%s". Directory not expected.') % (args[i], ))
filename = None
if filename is not None:
if len(revs) == 0:
@ -8483,3 +8411,5 @@ if __name__ == '__main__':
Gtk.main()
# save state
diff.saveState(statepath)
return 0

View File

@ -1,42 +1,28 @@
# Diffuse binary file
diffuse_conf = configuration_data()
diffuse_conf.set('SYSCONFIGDIR', sysconfdir)
pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name())
moduledir = join_paths(pkgdatadir, meson.project_name())
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(
input: 'usr/bin/diffuse.py.in',
input: 'diffuse.in',
output: 'diffuse',
configuration: diffuse_conf,
configuration: conf,
install: true,
install_dir: bindir
install_dir: get_option('bindir')
)
# Diffuse config file
diffuserc_conf = configuration_data()
diffuserc_conf.set('PKGDATADIR', pkgdatadir)
diffuse_sources = [
'__init__.py',
'main.py',
'utils.py',
]
configure_file(
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)
install_data(diffuse_sources, install_dir: moduledir)

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

@ -1,239 +1,239 @@
# Copyright (C) 2006-2014 Derrick Moser <derrick_moser@yahoo.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 licence, 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
# This program builds a Windows installer for Diffuse.
import codecs
import glob
import os
import platform
import subprocess
import sys
VERSION='0.6.0'
PACKAGE='1'
PLATFORM='win' + ''.join([ c for c in platform.architecture()[0] if c.isdigit() ])
INSTALLER='diffuse-%s-%s.%s' % (VERSION, PACKAGE, PLATFORM)
# makes a directory without complaining if it already exists
def mkdir(s):
if not os.path.isdir(s):
os.mkdir(s)
# copies a file to 'dest'
def copyFile(src, dest, use_text_mode=False,enc=None):
print 'copying "%s" to "%s"' % (src, dest)
if use_text_mode:
r, w = 'r', 'w'
else:
r, w = 'rb', 'wb'
f = open(src, r)
s = f.read()
f.close()
if enc is not None:
s = codecs.encode(unicode(s, 'utf_8'), enc)
f = open(dest, w)
f.write(s)
f.close()
# recursively copies a directory to 'dest'
def copyDir(src, dest):
print 'copying "%s" to "%s"' % (src, dest)
mkdir(dest)
for f in os.listdir(src):
s = os.path.join(src, f)
d = os.path.join(dest, f)
if os.path.isfile(s):
copyFile(s, d)
elif os.path.isdir(s):
copyDir(s, d)
# helper to clean up the resulting HTML
def extract_tag(s, start, end):
i = s.find(start)
if i >= 0:
pre = s[:i]
i += len(start)
j = s.find(end, i)
if j >= 0:
return pre, start, s[i:j], end, s[j+len(end):]
#
# Make sure we are in the correct directory.
#
path = os.path.dirname(sys.argv[0])
if path != '':
os.chdir(path)
#
# Build EXE versions of the Diffuse Python script.
#
# make a temp directory
mkdir('temp')
# copy script into temp directory under two names
for p in 'temp\\diffuse.py', 'temp\\diffusew.pyw':
copyFile('..\\src\\usr\\bin\\diffuse', p, True)
# build executable in 'dist' from diffuse.py and diffusew.pyw
args = [ sys.executable, 'setup.py', 'py2exe' ]
if os.spawnv(os.P_WAIT, args[0], args) != 0:
raise OSError('Could not run setup.py')
# include Microsoft redistributables needed by Python 2.6 and above
for f in 'msvcm90.dll', 'msvcp90.dll', 'msvcr90.dll':
copyFile(os.path.join(os.environ['SYSTEMROOT'], 'WinSxS\\x86_Microsoft.VC90.CRT_1fc8b3b9a1e18e3b_9.0.21022.8_x-ww_d08d0375\\' + f), 'dist\\' + f)
copyFile(os.path.join(os.environ['SYSTEMROOT'], 'WinSxS\\Manifests\\x86_Microsoft.VC90.CRT_1fc8b3b9a1e18e3b_9.0.21022.8_x-ww_d08d0375.manifest'), 'dist\\Microsoft.VC90.CRT.manifest')
# include GTK dependencies
gtk_dir = os.environ['GTK_BASEPATH']
copyDir(os.path.join(gtk_dir, 'etc'), 'dist\\etc')
copyDir(os.path.join(gtk_dir, 'lib'), 'dist\\lib')
mkdir('dist\\share')
copyDir(os.path.join(gtk_dir, 'share\\icons'), 'dist\\share\\icons')
copyDir(os.path.join(gtk_dir, 'share\\themes'), 'dist\\share\\themes')
#
# Add all support files.
#
# syntax highlighting support
mkdir('dist\\syntax')
for p in glob.glob('..\\src\\usr\\share\\diffuse\\syntax\\*.syntax'):
copyFile(p, os.path.join('dist\\syntax', os.path.basename(p)), True)
copyFile('diffuserc', 'dist\\diffuserc')
# application icon
copyDir('..\\src\\usr\\share\\icons', 'dist\\share\\icons')
# translations
mkdir('dist\\share\\locale')
locale_dir = os.path.join(gtk_dir, 'share\\locale')
for s in glob.glob('..\\po\\*.po'):
lang = s[16:-3]
# Diffuse localisations
print 'Compiling %s translation' % (lang, )
d = 'dist'
for p in [ 'locale', lang, 'LC_MESSAGES' ]:
d = os.path.join(d, p)
mkdir(d)
d = os.path.join(d, 'diffuse.mo')
if subprocess.Popen(['msgfmt', '-o', d, s]).wait() != 0:
raise OSError('Failed to compile "%s" into "%s".' % (s, d))
# GTK localisations
d = os.path.join(locale_dir, lang)
if os.path.isdir(d):
copyDir(d, os.path.join('dist\\share\\locale', lang))
#
# Add all documentation.
#
# license and other documentation
for p in 'AUTHORS', 'ChangeLog', 'COPYING', 'README':
copyFile(os.path.join('..', p), os.path.join('dist', p + '.txt'), True)
for p, enc in [ ('ChangeLog_ru', 'cp1251'), ('README_ru', 'cp1251') ]:
copyFile(os.path.join('..', p), os.path.join('dist', p + '.txt'), True, enc)
# fetch translations for English text hard coded in the stylesheets
translations = {}
f = open('translations.txt', 'rb')
for v in f.read().split('\n'):
v = v.split(':')
if len(v) == 3:
lang = v[0]
if not translations.has_key(lang):
translations[lang] = []
translations[lang].append(v[1:])
f.close()
# convert the manual from DocBook to HTML
d = '..\\src\\usr\\share\\gnome\\help\\diffuse'
for lang in os.listdir(d):
p = os.path.join(os.path.join(d, lang), 'diffuse.xml')
if os.path.isfile(p):
cmd = [ 'xsltproc', '/usr/share/sgml/docbook/xsl-stylesheets/html/docbook.xsl', p ]
info = subprocess.STARTUPINFO()
info.dwFlags |= subprocess.STARTF_USESHOWWINDOW
info.wShowWindow = subprocess.SW_HIDE
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=info)
proc.stdin.close()
proc.stderr.close()
fd = proc.stdout
s = fd.read()
fd.close()
if proc.wait() != 0:
raise OSError('Could not run xsltproc')
# add link to style sheet
s = s.replace('</head>', '<link rel="stylesheet" href="style.css" type="text/css"/></head>')
s = s.replace('<p>\n </p>', '')
s = s.replace('<p>\n </p>', '')
# cleanup HTML to simpler UTF-8 form
s = s.replace('<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">', '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">')
a, idx = [], 0
while True:
i = s.find('&#', idx)
if i < 0:
a.append(unicode(s[idx:], 'latin_1'))
break
a.append(unicode(s[idx:i], 'latin_1'))
i += 2
j = s.find(';', i)
a.append(unichr(int(s[i:j])))
idx = j + 1
s = u''.join(a)
s = codecs.encode(s, 'utf-8')
# clean up translator credit portion
div = extract_tag(s, '<div class="othercredit">', '</div>')
if div is not None:
firstname = extract_tag(div[2], '<span class="firstname">', '</span>')
surname = extract_tag(div[2], '<span class="surname">', '</span>')
contrib = extract_tag(div[2], '<span class="contrib">', '</span>')
email = extract_tag(div[2], '<code class="email">', '</code>')
copyright = extract_tag(div[4], '<p class="copyright">', '</p>')
if firstname is not None and surname is not None and contrib is not None and email is not None and copyright is not None:
s = '%s%s<p><span class="contrib">%s:</span> <span class="firstname">%s</span> <span class="surname">%s</span> <code class="email">%s</code></p>%s' % (div[0], ''.join(copyright[:4]), contrib[2], firstname[2], surname[2], email[2], copyright[4])
# translate extra text
for k, v in translations.get(lang, []):
s = s.replace(k, v)
# save HTML version of the manual
fn = 'manual'
if lang != 'C':
fn += '_' + lang
# update the document language
s = s.replace(' lang="en" ', ' lang="%s" ' % (lang,))
f = open(os.path.join('dist', fn + '.html'), 'w')
f.write(s)
f.close()
copyFile('style.css', 'dist\\style.css')
#
# Package everything into a single EXE installer.
#
# build binary installer
copyFile(os.path.join(os.environ['ADD_PATH_HOME'], 'add_path.exe'), 'dist\\add_path.exe')
if os.system('iscc diffuse.iss /F%s' % (INSTALLER, )) != 0:
raise OSError('Could not run iscc')
#
# Declare success.
#
print 'Successfully created "%s".' % (INSTALLER, )
# Copyright (C) 2006-2014 Derrick Moser <derrick_moser@yahoo.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 licence, 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
# This program builds a Windows installer for Diffuse.
import codecs
import glob
import os
import platform
import subprocess
import sys
VERSION='0.6.0'
PACKAGE='1'
PLATFORM='win' + ''.join([ c for c in platform.architecture()[0] if c.isdigit() ])
INSTALLER='diffuse-%s-%s.%s' % (VERSION, PACKAGE, PLATFORM)
# makes a directory without complaining if it already exists
def mkdir(s):
if not os.path.isdir(s):
os.mkdir(s)
# copies a file to 'dest'
def copyFile(src, dest, use_text_mode=False,enc=None):
print 'copying "%s" to "%s"' % (src, dest)
if use_text_mode:
r, w = 'r', 'w'
else:
r, w = 'rb', 'wb'
f = open(src, r)
s = f.read()
f.close()
if enc is not None:
s = codecs.encode(unicode(s, 'utf_8'), enc)
f = open(dest, w)
f.write(s)
f.close()
# recursively copies a directory to 'dest'
def copyDir(src, dest):
print 'copying "%s" to "%s"' % (src, dest)
mkdir(dest)
for f in os.listdir(src):
s = os.path.join(src, f)
d = os.path.join(dest, f)
if os.path.isfile(s):
copyFile(s, d)
elif os.path.isdir(s):
copyDir(s, d)
# helper to clean up the resulting HTML
def extract_tag(s, start, end):
i = s.find(start)
if i >= 0:
pre = s[:i]
i += len(start)
j = s.find(end, i)
if j >= 0:
return pre, start, s[i:j], end, s[j+len(end):]
#
# Make sure we are in the correct directory.
#
path = os.path.dirname(sys.argv[0])
if path != '':
os.chdir(path)
#
# Build EXE versions of the Diffuse Python script.
#
# make a temp directory
mkdir('temp')
# copy script into temp directory under two names
for p in 'temp\\diffuse.py', 'temp\\diffusew.pyw':
copyFile('..\\src\\usr\\bin\\diffuse', p, True)
# build executable in 'dist' from diffuse.py and diffusew.pyw
args = [ sys.executable, 'setup.py', 'py2exe' ]
if os.spawnv(os.P_WAIT, args[0], args) != 0:
raise OSError('Could not run setup.py')
# include Microsoft redistributables needed by Python 2.6 and above
for f in 'msvcm90.dll', 'msvcp90.dll', 'msvcr90.dll':
copyFile(os.path.join(os.environ['SYSTEMROOT'], 'WinSxS\\x86_Microsoft.VC90.CRT_1fc8b3b9a1e18e3b_9.0.21022.8_x-ww_d08d0375\\' + f), 'dist\\' + f)
copyFile(os.path.join(os.environ['SYSTEMROOT'], 'WinSxS\\Manifests\\x86_Microsoft.VC90.CRT_1fc8b3b9a1e18e3b_9.0.21022.8_x-ww_d08d0375.manifest'), 'dist\\Microsoft.VC90.CRT.manifest')
# include GTK dependencies
gtk_dir = os.environ['GTK_BASEPATH']
copyDir(os.path.join(gtk_dir, 'etc'), 'dist\\etc')
copyDir(os.path.join(gtk_dir, 'lib'), 'dist\\lib')
mkdir('dist\\share')
copyDir(os.path.join(gtk_dir, 'share\\icons'), 'dist\\share\\icons')
copyDir(os.path.join(gtk_dir, 'share\\themes'), 'dist\\share\\themes')
#
# Add all support files.
#
# syntax highlighting support
mkdir('dist\\syntax')
for p in glob.glob('..\\src\\usr\\share\\diffuse\\syntax\\*.syntax'):
copyFile(p, os.path.join('dist\\syntax', os.path.basename(p)), True)
copyFile('diffuserc', 'dist\\diffuserc')
# application icon
copyDir('..\\src\\usr\\share\\icons', 'dist\\share\\icons')
# translations
mkdir('dist\\share\\locale')
locale_dir = os.path.join(gtk_dir, 'share\\locale')
for s in glob.glob('..\\po\\*.po'):
lang = s[16:-3]
# Diffuse localisations
print 'Compiling %s translation' % (lang, )
d = 'dist'
for p in [ 'locale', lang, 'LC_MESSAGES' ]:
d = os.path.join(d, p)
mkdir(d)
d = os.path.join(d, 'diffuse.mo')
if subprocess.Popen(['msgfmt', '-o', d, s]).wait() != 0:
raise OSError('Failed to compile "%s" into "%s".' % (s, d))
# GTK localisations
d = os.path.join(locale_dir, lang)
if os.path.isdir(d):
copyDir(d, os.path.join('dist\\share\\locale', lang))
#
# Add all documentation.
#
# license and other documentation
for p in 'AUTHORS', 'ChangeLog', 'COPYING', 'README':
copyFile(os.path.join('..', p), os.path.join('dist', p + '.txt'), True)
for p, enc in [ ('ChangeLog_ru', 'cp1251'), ('README_ru', 'cp1251') ]:
copyFile(os.path.join('..', p), os.path.join('dist', p + '.txt'), True, enc)
# fetch translations for English text hard coded in the stylesheets
translations = {}
f = open('translations.txt', 'rb')
for v in f.read().split('\n'):
v = v.split(':')
if len(v) == 3:
lang = v[0]
if not translations.has_key(lang):
translations[lang] = []
translations[lang].append(v[1:])
f.close()
# convert the manual from DocBook to HTML
d = '..\\src\\usr\\share\\gnome\\help\\diffuse'
for lang in os.listdir(d):
p = os.path.join(os.path.join(d, lang), 'diffuse.xml')
if os.path.isfile(p):
cmd = [ 'xsltproc', '/usr/share/sgml/docbook/xsl-stylesheets/html/docbook.xsl', p ]
info = subprocess.STARTUPINFO()
info.dwFlags |= subprocess.STARTF_USESHOWWINDOW
info.wShowWindow = subprocess.SW_HIDE
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=info)
proc.stdin.close()
proc.stderr.close()
fd = proc.stdout
s = fd.read()
fd.close()
if proc.wait() != 0:
raise OSError('Could not run xsltproc')
# add link to style sheet
s = s.replace('</head>', '<link rel="stylesheet" href="style.css" type="text/css"/></head>')
s = s.replace('<p>\n </p>', '')
s = s.replace('<p>\n </p>', '')
# cleanup HTML to simpler UTF-8 form
s = s.replace('<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">', '<meta http-equiv="Content-Type" content="text/html; charset=utf-8">')
a, idx = [], 0
while True:
i = s.find('&#', idx)
if i < 0:
a.append(unicode(s[idx:], 'latin_1'))
break
a.append(unicode(s[idx:i], 'latin_1'))
i += 2
j = s.find(';', i)
a.append(unichr(int(s[i:j])))
idx = j + 1
s = ''.join(a)
s = codecs.encode(s, 'utf-8')
# clean up translator credit portion
div = extract_tag(s, '<div class="othercredit">', '</div>')
if div is not None:
firstname = extract_tag(div[2], '<span class="firstname">', '</span>')
surname = extract_tag(div[2], '<span class="surname">', '</span>')
contrib = extract_tag(div[2], '<span class="contrib">', '</span>')
email = extract_tag(div[2], '<code class="email">', '</code>')
copyright = extract_tag(div[4], '<p class="copyright">', '</p>')
if firstname is not None and surname is not None and contrib is not None and email is not None and copyright is not None:
s = '%s%s<p><span class="contrib">%s:</span> <span class="firstname">%s</span> <span class="surname">%s</span> <code class="email">%s</code></p>%s' % (div[0], ''.join(copyright[:4]), contrib[2], firstname[2], surname[2], email[2], copyright[4])
# translate extra text
for k, v in translations.get(lang, []):
s = s.replace(k, v)
# save HTML version of the manual
fn = 'manual'
if lang != 'C':
fn += '_' + lang
# update the document language
s = s.replace(' lang="en" ', ' lang="%s" ' % (lang,))
f = open(os.path.join('dist', fn + '.html'), 'w')
f.write(s)
f.close()
copyFile('style.css', 'dist\\style.css')
#
# Package everything into a single EXE installer.
#
# build binary installer
copyFile(os.path.join(os.environ['ADD_PATH_HOME'], 'add_path.exe'), 'dist\\add_path.exe')
if os.system('iscc diffuse.iss /F%s' % (INSTALLER, )) != 0:
raise OSError('Could not run iscc')
#
# Declare success.
#
print 'Successfully created "%s".' % (INSTALLER, )